aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormax42 <max42@yandex-team.com>2023-07-29 00:02:16 +0300
committermax42 <max42@yandex-team.com>2023-07-29 00:02:16 +0300
commit73b89de71748a21e102d27b9f3ed1bf658766cb5 (patch)
tree188bbd2d622fa91cdcbb1b6d6d77fbc84a0646f5
parent528e321bcc2a2b67b53aeba58c3bd88305a141ee (diff)
downloadydb-73b89de71748a21e102d27b9f3ed1bf658766cb5.tar.gz
YT-19210: expose YQL shared library for YT.
After this, a new target libyqlplugin.so appears. in open-source cmake build. Diff in open-source YDB repo looks like the following: https://paste.yandex-team.ru/f302bdb4-7ef2-4362-91c7-6ca45f329264
-rw-r--r--CMakeLists.darwin-x86_64.txt3
-rw-r--r--CMakeLists.linux-aarch64.txt3
-rw-r--r--CMakeLists.linux-x86_64.txt3
-rw-r--r--CMakeLists.txt1
-rw-r--r--CMakeLists.windows-x86_64.txt3
-rw-r--r--cmake/shared_libs.cmake9
-rw-r--r--contrib/libs/CMakeLists.darwin-x86_64.txt1
-rw-r--r--contrib/libs/CMakeLists.linux-aarch64.txt1
-rw-r--r--contrib/libs/CMakeLists.linux-x86_64.txt1
-rw-r--r--contrib/libs/CMakeLists.windows-x86_64.txt1
-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/linux-headers/linux/bpf_common.h57
-rw-r--r--contrib/libs/linux-headers/linux/filter.h90
-rw-r--r--contrib/libs/yajl/CMakeLists.darwin-x86_64.txt29
-rw-r--r--contrib/libs/yajl/CMakeLists.linux-aarch64.txt30
-rw-r--r--contrib/libs/yajl/CMakeLists.linux-x86_64.txt30
-rw-r--r--contrib/libs/yajl/CMakeLists.txt17
-rw-r--r--contrib/libs/yajl/CMakeLists.windows-x86_64.txt29
-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/CMakeLists.darwin-x86_64.txt1
-rw-r--r--contrib/restricted/CMakeLists.linux-aarch64.txt1
-rw-r--r--contrib/restricted/CMakeLists.linux-x86_64.txt1
-rw-r--r--contrib/restricted/CMakeLists.windows-x86_64.txt1
-rw-r--r--contrib/restricted/http-parser/AUTHORS68
-rw-r--r--contrib/restricted/http-parser/CMakeLists.darwin-x86_64.txt21
-rw-r--r--contrib/restricted/http-parser/CMakeLists.linux-aarch64.txt24
-rw-r--r--contrib/restricted/http-parser/CMakeLists.linux-x86_64.txt24
-rw-r--r--contrib/restricted/http-parser/CMakeLists.txt17
-rw-r--r--contrib/restricted/http-parser/CMakeLists.windows-x86_64.txt21
-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--library/cpp/CMakeLists.darwin-x86_64.txt2
-rw-r--r--library/cpp/CMakeLists.linux-aarch64.txt2
-rw-r--r--library/cpp/CMakeLists.linux-x86_64.txt2
-rw-r--r--library/cpp/CMakeLists.windows-x86_64.txt2
-rw-r--r--library/cpp/containers/concurrent_hash/concurrent_hash.h128
-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/skiff/CMakeLists.darwin-x86_64.txt32
-rw-r--r--library/cpp/skiff/CMakeLists.linux-aarch64.txt33
-rw-r--r--library/cpp/skiff/CMakeLists.linux-x86_64.txt33
-rw-r--r--library/cpp/skiff/CMakeLists.txt17
-rw-r--r--library/cpp/skiff/CMakeLists.windows-x86_64.txt32
-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/threading/CMakeLists.txt3
-rw-r--r--library/cpp/threading/blocking_queue/CMakeLists.darwin-x86_64.txt17
-rw-r--r--library/cpp/threading/blocking_queue/CMakeLists.linux-aarch64.txt18
-rw-r--r--library/cpp/threading/blocking_queue/CMakeLists.linux-x86_64.txt18
-rw-r--r--library/cpp/threading/blocking_queue/CMakeLists.txt17
-rw-r--r--library/cpp/threading/blocking_queue/CMakeLists.windows-x86_64.txt17
-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/CMakeLists.darwin-x86_64.txt18
-rw-r--r--library/cpp/threading/cron/CMakeLists.linux-aarch64.txt19
-rw-r--r--library/cpp/threading/cron/CMakeLists.linux-x86_64.txt19
-rw-r--r--library/cpp/threading/cron/CMakeLists.txt17
-rw-r--r--library/cpp/threading/cron/CMakeLists.windows-x86_64.txt18
-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/threading/thread_local/CMakeLists.darwin-x86_64.txt31
-rw-r--r--library/cpp/threading/thread_local/CMakeLists.linux-aarch64.txt32
-rw-r--r--library/cpp/threading/thread_local/CMakeLists.linux-x86_64.txt32
-rw-r--r--library/cpp/threading/thread_local/CMakeLists.txt17
-rw-r--r--library/cpp/threading/thread_local/CMakeLists.windows-x86_64.txt31
-rw-r--r--library/cpp/type_info/CMakeLists.darwin-x86_64.txt38
-rw-r--r--library/cpp/type_info/CMakeLists.linux-aarch64.txt39
-rw-r--r--library/cpp/type_info/CMakeLists.linux-x86_64.txt39
-rw-r--r--library/cpp/type_info/CMakeLists.txt17
-rw-r--r--library/cpp/type_info/CMakeLists.windows-x86_64.txt38
-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/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/README.txt5
-rw-r--r--library/cpp/type_info/ut/test-data/bad-types.txt229
-rw-r--r--library/cpp/type_info/ut/test-data/good-types.txt478
-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/CMakeLists.txt7
-rw-r--r--library/cpp/yt/backtrace/CMakeLists.darwin-x86_64.txt23
-rw-r--r--library/cpp/yt/backtrace/CMakeLists.linux-aarch64.txt24
-rw-r--r--library/cpp/yt/backtrace/CMakeLists.linux-x86_64.txt24
-rw-r--r--library/cpp/yt/backtrace/CMakeLists.txt17
-rw-r--r--library/cpp/yt/backtrace/CMakeLists.windows-x86_64.txt20
-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/CMakeLists.darwin-x86_64.txt9
-rw-r--r--library/cpp/yt/backtrace/cursors/CMakeLists.linux-aarch64.txt9
-rw-r--r--library/cpp/yt/backtrace/cursors/CMakeLists.linux-x86_64.txt9
-rw-r--r--library/cpp/yt/backtrace/cursors/CMakeLists.txt17
-rw-r--r--library/cpp/yt/backtrace/cursors/CMakeLists.windows-x86_64.txt9
-rw-r--r--library/cpp/yt/backtrace/cursors/dummy/CMakeLists.txt11
-rw-r--r--library/cpp/yt/backtrace/cursors/dummy/CMakeLists.windows-x86_64.txt17
-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/CMakeLists.darwin-x86_64.txt21
-rw-r--r--library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-aarch64.txt22
-rw-r--r--library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-x86_64.txt22
-rw-r--r--library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.txt15
-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/CMakeLists.darwin-x86_64.txt15
-rw-r--r--library/cpp/yt/containers/CMakeLists.linux-aarch64.txt16
-rw-r--r--library/cpp/yt/containers/CMakeLists.linux-x86_64.txt16
-rw-r--r--library/cpp/yt/containers/CMakeLists.txt17
-rw-r--r--library/cpp/yt/containers/CMakeLists.windows-x86_64.txt15
-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/CMakeLists.darwin-x86_64.txt21
-rw-r--r--library/cpp/yt/cpu_clock/CMakeLists.linux-aarch64.txt22
-rw-r--r--library/cpp/yt/cpu_clock/CMakeLists.linux-x86_64.txt22
-rw-r--r--library/cpp/yt/cpu_clock/CMakeLists.txt17
-rw-r--r--library/cpp/yt/cpu_clock/CMakeLists.windows-x86_64.txt18
-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/CMakeLists.darwin-x86_64.txt25
-rw-r--r--library/cpp/yt/logging/CMakeLists.linux-aarch64.txt26
-rw-r--r--library/cpp/yt/logging/CMakeLists.linux-x86_64.txt26
-rw-r--r--library/cpp/yt/logging/CMakeLists.txt17
-rw-r--r--library/cpp/yt/logging/CMakeLists.windows-x86_64.txt22
-rw-r--r--library/cpp/yt/logging/backends/arcadia/backend.cpp86
-rw-r--r--library/cpp/yt/logging/backends/arcadia/backend.h18
-rw-r--r--library/cpp/yt/logging/backends/arcadia/ya.make16
-rw-r--r--library/cpp/yt/logging/backends/stream/stream_log_manager.cpp87
-rw-r--r--library/cpp/yt/logging/backends/stream/stream_log_manager.h15
-rw-r--r--library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp37
-rw-r--r--library/cpp/yt/logging/backends/stream/unittests/ya.make14
-rw-r--r--library/cpp/yt/logging/backends/stream/ya.make20
-rw-r--r--library/cpp/yt/logging/backends/ya.make4
-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/plain_text_formatter/CMakeLists.darwin-x86_64.txt24
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-aarch64.txt25
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-x86_64.txt25
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/CMakeLists.txt17
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/CMakeLists.windows-x86_64.txt21
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/formatter.cpp226
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/formatter.h48
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/ya.make16
-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.make25
-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.h308
-rw-r--r--library/cpp/yt/string/raw_formatter.h212
-rw-r--r--library/cpp/yt/system/CMakeLists.darwin-x86_64.txt20
-rw-r--r--library/cpp/yt/system/CMakeLists.linux-aarch64.txt21
-rw-r--r--library/cpp/yt/system/CMakeLists.linux-x86_64.txt21
-rw-r--r--library/cpp/yt/system/CMakeLists.txt17
-rw-r--r--library/cpp/yt/system/CMakeLists.windows-x86_64.txt17
-rw-r--r--library/cpp/yt/threading/CMakeLists.darwin-x86_64.txt37
-rw-r--r--library/cpp/yt/threading/CMakeLists.linux-aarch64.txt38
-rw-r--r--library/cpp/yt/threading/CMakeLists.linux-x86_64.txt38
-rw-r--r--library/cpp/yt/threading/CMakeLists.txt17
-rw-r--r--library/cpp/yt/threading/CMakeLists.windows-x86_64.txt34
-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/CMakeLists.darwin-x86_64.txt18
-rw-r--r--library/cpp/yt/user_job_statistics/CMakeLists.linux-aarch64.txt19
-rw-r--r--library/cpp/yt/user_job_statistics/CMakeLists.linux-x86_64.txt19
-rw-r--r--library/cpp/yt/user_job_statistics/CMakeLists.txt17
-rw-r--r--library/cpp/yt/user_job_statistics/CMakeLists.windows-x86_64.txt18
-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/CMakeLists.darwin-x86_64.txt1
-rw-r--r--ydb/library/yql/core/CMakeLists.linux-aarch64.txt1
-rw-r--r--ydb/library/yql/core/CMakeLists.linux-x86_64.txt1
-rw-r--r--ydb/library/yql/core/CMakeLists.windows-x86_64.txt1
-rw-r--r--ydb/library/yql/core/url_preprocessing/CMakeLists.darwin-x86_64.txt22
-rw-r--r--ydb/library/yql/core/url_preprocessing/CMakeLists.linux-aarch64.txt23
-rw-r--r--ydb/library/yql/core/url_preprocessing/CMakeLists.linux-x86_64.txt23
-rw-r--r--ydb/library/yql/core/url_preprocessing/CMakeLists.txt17
-rw-r--r--ydb/library/yql/core/url_preprocessing/CMakeLists.windows-x86_64.txt22
-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.make16
-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/CMakeLists.txt2
-rw-r--r--ydb/library/yql/providers/stat/CMakeLists.txt10
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/CMakeLists.darwin-x86_64.txt41
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-aarch64.txt42
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-x86_64.txt42
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/CMakeLists.windows-x86_64.txt41
-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/CMakeLists.darwin-x86_64.txt18
-rw-r--r--ydb/library/yql/providers/stat/uploader/CMakeLists.linux-aarch64.txt19
-rw-r--r--ydb/library/yql/providers/stat/uploader/CMakeLists.linux-x86_64.txt19
-rw-r--r--ydb/library/yql/providers/stat/uploader/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/stat/uploader/CMakeLists.windows-x86_64.txt18
-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/yt/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/codec/CMakeLists.darwin-x86_64.txt38
-rw-r--r--ydb/library/yql/providers/yt/codec/CMakeLists.linux-aarch64.txt39
-rw-r--r--ydb/library/yql/providers/yt/codec/CMakeLists.linux-x86_64.txt39
-rw-r--r--ydb/library/yql/providers/yt/codec/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/codec/CMakeLists.windows-x86_64.txt38
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/CMakeLists.darwin-x86_64.txt105
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-aarch64.txt107
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-x86_64.txt107
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/CMakeLists.windows-x86_64.txt105
-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.cpp2277
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec_io.h163
-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/CMakeLists.darwin-x86_64.txt42
-rw-r--r--ydb/library/yql/providers/yt/common/CMakeLists.linux-aarch64.txt43
-rw-r--r--ydb/library/yql/providers/yt/common/CMakeLists.linux-x86_64.txt43
-rw-r--r--ydb/library/yql/providers/yt/common/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/common/CMakeLists.windows-x86_64.txt42
-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.h66
-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.cpp511
-rw-r--r--ydb/library/yql/providers/yt/common/yql_yt_settings.h343
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/CMakeLists.darwin-x86_64.txt34
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-aarch64.txt35
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-x86_64.txt35
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/CMakeLists.windows-x86_64.txt34
-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/CMakeLists.darwin-x86_64.txt41
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-aarch64.txt42
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-x86_64.txt42
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/CMakeLists.windows-x86_64.txt41
-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/CMakeLists.txt10
-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/CMakeLists.darwin-x86_64.txt44
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-aarch64.txt45
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-x86_64.txt45
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/CMakeLists.windows-x86_64.txt44
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp380
-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.cpp465
-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/CMakeLists.darwin-x86_64.txt73
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-aarch64.txt74
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-x86_64.txt74
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/CMakeLists.windows-x86_64.txt73
-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.cpp4908
-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.cpp582
-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/job/CMakeLists.darwin-x86_64.txt47
-rw-r--r--ydb/library/yql/providers/yt/job/CMakeLists.linux-aarch64.txt48
-rw-r--r--ydb/library/yql/providers/yt/job/CMakeLists.linux-x86_64.txt48
-rw-r--r--ydb/library/yql/providers/yt/job/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/job/CMakeLists.windows-x86_64.txt47
-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/CMakeLists.txt25
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.darwin-x86_64.txt18
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-aarch64.txt19
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-x86_64.txt19
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.windows-x86_64.txt18
-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/CMakeLists.darwin-x86_64.txt28
-rw-r--r--ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-aarch64.txt29
-rw-r--r--ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-x86_64.txt29
-rw-r--r--ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.windows-x86_64.txt28
-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/CMakeLists.darwin-x86_64.txt26
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-aarch64.txt27
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-x86_64.txt27
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.windows-x86_64.txt26
-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/CMakeLists.darwin-x86_64.txt24
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-aarch64.txt25
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-x86_64.txt25
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/CMakeLists.windows-x86_64.txt24
-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/CMakeLists.darwin-x86_64.txt22
-rw-r--r--ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-aarch64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-x86_64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.windows-x86_64.txt22
-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/CMakeLists.darwin-x86_64.txt21
-rw-r--r--ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-aarch64.txt22
-rw-r--r--ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-x86_64.txt22
-rw-r--r--ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.windows-x86_64.txt21
-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/CMakeLists.darwin-x86_64.txt24
-rw-r--r--ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-aarch64.txt25
-rw-r--r--ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-x86_64.txt25
-rw-r--r--ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.windows-x86_64.txt24
-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/CMakeLists.darwin-x86_64.txt27
-rw-r--r--ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-aarch64.txt28
-rw-r--r--ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-x86_64.txt28
-rw-r--r--ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.windows-x86_64.txt27
-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/CMakeLists.darwin-x86_64.txt20
-rw-r--r--ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-aarch64.txt21
-rw-r--r--ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-x86_64.txt21
-rw-r--r--ydb/library/yql/providers/yt/lib/log/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/log/CMakeLists.windows-x86_64.txt20
-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/CMakeLists.darwin-x86_64.txt24
-rw-r--r--ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-aarch64.txt25
-rw-r--r--ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-x86_64.txt25
-rw-r--r--ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.windows-x86_64.txt24
-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/CMakeLists.darwin-x86_64.txt28
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-aarch64.txt29
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-x86_64.txt29
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.windows-x86_64.txt28
-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/CMakeLists.darwin-x86_64.txt32
-rw-r--r--ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-aarch64.txt33
-rw-r--r--ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-x86_64.txt33
-rw-r--r--ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.windows-x86_64.txt32
-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.cpp1643
-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/CMakeLists.darwin-x86_64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-aarch64.txt24
-rw-r--r--ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-x86_64.txt24
-rw-r--r--ydb/library/yql/providers/yt/lib/schema/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/schema/CMakeLists.windows-x86_64.txt23
-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/CMakeLists.darwin-x86_64.txt22
-rw-r--r--ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-aarch64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-x86_64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/skiff/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/skiff/CMakeLists.windows-x86_64.txt22
-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/CMakeLists.darwin-x86_64.txt21
-rw-r--r--ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-aarch64.txt22
-rw-r--r--ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-x86_64.txt22
-rw-r--r--ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.windows-x86_64.txt21
-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/yson_helpers/CMakeLists.darwin-x86_64.txt22
-rw-r--r--ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-aarch64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-x86_64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.windows-x86_64.txt22
-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/lib/yt_download/CMakeLists.darwin-x86_64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-aarch64.txt24
-rw-r--r--ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-x86_64.txt24
-rw-r--r--ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.windows-x86_64.txt23
-rw-r--r--ydb/library/yql/providers/yt/lib/yt_download/ya.make16
-rw-r--r--ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp111
-rw-r--r--ydb/library/yql/providers/yt/lib/yt_download/yt_download.h11
-rw-r--r--ydb/library/yql/providers/yt/opt/CMakeLists.darwin-x86_64.txt25
-rw-r--r--ydb/library/yql/providers/yt/opt/CMakeLists.linux-aarch64.txt26
-rw-r--r--ydb/library/yql/providers/yt/opt/CMakeLists.linux-x86_64.txt26
-rw-r--r--ydb/library/yql/providers/yt/opt/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/opt/CMakeLists.windows-x86_64.txt25
-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/CMakeLists.darwin-x86_64.txt113
-rw-r--r--ydb/library/yql/providers/yt/provider/CMakeLists.linux-aarch64.txt114
-rw-r--r--ydb/library/yql/providers/yt/provider/CMakeLists.linux-x86_64.txt114
-rw-r--r--ydb/library/yql/providers/yt/provider/CMakeLists.txt17
-rw-r--r--ydb/library/yql/providers/yt/provider/CMakeLists.windows-x86_64.txt113
-rw-r--r--ydb/library/yql/providers/yt/provider/ut/ya.make34
-rw-r--r--ydb/library/yql/providers/yt/provider/ut/yql_yt_cbo_ut.cpp137
-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.make101
-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.cpp850
-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.cpp2141
-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.cpp4762
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_join_impl.h68
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp346
-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.cpp2661
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp537
-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.cpp676
-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.cpp2559
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp7414
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp508
-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.cpp2991
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_table.h353
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp398
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_table_desc.h78
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp256
-rw-r--r--yt/CMakeLists.txt12
-rw-r--r--yt/cpp/CMakeLists.txt9
-rw-r--r--yt/cpp/mapreduce/CMakeLists.txt16
-rw-r--r--yt/cpp/mapreduce/client/CMakeLists.darwin-x86_64.txt70
-rw-r--r--yt/cpp/mapreduce/client/CMakeLists.linux-aarch64.txt70
-rw-r--r--yt/cpp/mapreduce/client/CMakeLists.linux-x86_64.txt71
-rw-r--r--yt/cpp/mapreduce/client/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/client/CMakeLists.windows-x86_64.txt67
-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/CMakeLists.darwin-x86_64.txt31
-rw-r--r--yt/cpp/mapreduce/common/CMakeLists.linux-aarch64.txt32
-rw-r--r--yt/cpp/mapreduce/common/CMakeLists.linux-x86_64.txt32
-rw-r--r--yt/cpp/mapreduce/common/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/common/CMakeLists.windows-x86_64.txt28
-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/CMakeLists.darwin-x86_64.txt37
-rw-r--r--yt/cpp/mapreduce/http/CMakeLists.linux-aarch64.txt38
-rw-r--r--yt/cpp/mapreduce/http/CMakeLists.linux-x86_64.txt38
-rw-r--r--yt/cpp/mapreduce/http/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/http/CMakeLists.windows-x86_64.txt34
-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/CMakeLists.darwin-x86_64.txt143
-rw-r--r--yt/cpp/mapreduce/interface/CMakeLists.linux-aarch64.txt144
-rw-r--r--yt/cpp/mapreduce/interface/CMakeLists.linux-x86_64.txt144
-rw-r--r--yt/cpp/mapreduce/interface/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/interface/CMakeLists.windows-x86_64.txt140
-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/CMakeLists.darwin-x86_64.txt34
-rw-r--r--yt/cpp/mapreduce/interface/logging/CMakeLists.linux-aarch64.txt35
-rw-r--r--yt/cpp/mapreduce/interface/logging/CMakeLists.linux-x86_64.txt35
-rw-r--r--yt/cpp/mapreduce/interface/logging/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/interface/logging/CMakeLists.windows-x86_64.txt31
-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/CMakeLists.darwin-x86_64.txt41
-rw-r--r--yt/cpp/mapreduce/io/CMakeLists.linux-aarch64.txt42
-rw-r--r--yt/cpp/mapreduce/io/CMakeLists.linux-x86_64.txt42
-rw-r--r--yt/cpp/mapreduce/io/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/io/CMakeLists.windows-x86_64.txt38
-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/CMakeLists.txt9
-rw-r--r--yt/cpp/mapreduce/library/table_schema/CMakeLists.darwin-x86_64.txt21
-rw-r--r--yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-aarch64.txt22
-rw-r--r--yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-x86_64.txt22
-rw-r--r--yt/cpp/mapreduce/library/table_schema/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/library/table_schema/CMakeLists.windows-x86_64.txt18
-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/CMakeLists.darwin-x86_64.txt27
-rw-r--r--yt/cpp/mapreduce/raw_client/CMakeLists.linux-aarch64.txt28
-rw-r--r--yt/cpp/mapreduce/raw_client/CMakeLists.linux-x86_64.txt28
-rw-r--r--yt/cpp/mapreduce/raw_client/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/raw_client/CMakeLists.windows-x86_64.txt24
-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/CMakeLists.darwin-x86_64.txt15
-rw-r--r--yt/cpp/mapreduce/skiff/CMakeLists.linux-aarch64.txt16
-rw-r--r--yt/cpp/mapreduce/skiff/CMakeLists.linux-x86_64.txt16
-rw-r--r--yt/cpp/mapreduce/skiff/CMakeLists.txt17
-rw-r--r--yt/cpp/mapreduce/skiff/CMakeLists.windows-x86_64.txt15
-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/gradle.inc4
-rw-r--r--yt/opensource.inc21
-rw-r--r--yt/opensource_tests.inc24
-rw-r--r--yt/ya_cpp.make.inc2
-rw-r--r--yt/yql/CMakeLists.txt9
-rw-r--r--yt/yql/plugin/CMakeLists.darwin-x86_64.txt19
-rw-r--r--yt/yql/plugin/CMakeLists.linux-aarch64.txt20
-rw-r--r--yt/yql/plugin/CMakeLists.linux-x86_64.txt20
-rw-r--r--yt/yql/plugin/CMakeLists.txt17
-rw-r--r--yt/yql/plugin/CMakeLists.windows-x86_64.txt19
-rw-r--r--yt/yql/plugin/bridge/interface.h74
-rw-r--r--yt/yql/plugin/bridge/plugin.cpp120
-rw-r--r--yt/yql/plugin/bridge/plugin.h13
-rw-r--r--yt/yql/plugin/bridge/ya.make11
-rw-r--r--yt/yql/plugin/dynamic/CMakeLists.darwin-x86_64.txt31
-rw-r--r--yt/yql/plugin/dynamic/CMakeLists.linux-aarch64.txt35
-rw-r--r--yt/yql/plugin/dynamic/CMakeLists.linux-x86_64.txt35
-rw-r--r--yt/yql/plugin/dynamic/CMakeLists.txt17
-rw-r--r--yt/yql/plugin/dynamic/CMakeLists.windows-x86_64.txt22
-rw-r--r--yt/yql/plugin/dynamic/dylib.exports12
-rw-r--r--yt/yql/plugin/dynamic/impl.cpp93
-rw-r--r--yt/yql/plugin/dynamic/ya.make15
-rw-r--r--yt/yql/plugin/native/CMakeLists.darwin-x86_64.txt92
-rw-r--r--yt/yql/plugin/native/CMakeLists.linux-aarch64.txt94
-rw-r--r--yt/yql/plugin/native/CMakeLists.linux-x86_64.txt94
-rw-r--r--yt/yql/plugin/native/CMakeLists.txt17
-rw-r--r--yt/yql/plugin/native/CMakeLists.windows-x86_64.txt92
-rw-r--r--yt/yql/plugin/native/error_helpers.cpp121
-rw-r--r--yt/yql/plugin/native/error_helpers.h17
-rw-r--r--yt/yql/plugin/native/plugin.cpp293
-rw-r--r--yt/yql/plugin/native/plugin.h14
-rw-r--r--yt/yql/plugin/native/ya.make44
-rw-r--r--yt/yql/plugin/plugin.cpp18
-rw-r--r--yt/yql/plugin/plugin.h65
-rw-r--r--yt/yql/plugin/ya.make19
-rw-r--r--yt/yt/CMakeLists.txt11
-rw-r--r--yt/yt/build/CMakeLists.darwin-x86_64.txt96
-rw-r--r--yt/yt/build/CMakeLists.linux-aarch64.txt97
-rw-r--r--yt/yt/build/CMakeLists.linux-x86_64.txt97
-rw-r--r--yt/yt/build/CMakeLists.txt17
-rw-r--r--yt/yt/build/CMakeLists.windows-x86_64.txt93
-rw-r--r--yt/yt/build/build.cpp.in59
-rw-r--r--yt/yt/build/build.h19
-rw-r--r--yt/yt/build/config.h.in49
-rw-r--r--yt/yt/build/ya.make45
-rw-r--r--yt/yt/build/ya_version.cpp158
-rw-r--r--yt/yt/build/ya_version.h19
-rw-r--r--yt/yt/core/CMakeLists.darwin-x86_64.txt366
-rw-r--r--yt/yt/core/CMakeLists.linux-aarch64.txt366
-rw-r--r--yt/yt/core/CMakeLists.linux-x86_64.txt368
-rw-r--r--yt/yt/core/CMakeLists.txt17
-rw-r--r--yt/yt/core/CMakeLists.windows-x86_64.txt364
-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.cpp262
-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.h99
-rw-r--r--yt/yt/core/actions/invoker_detail.cpp73
-rw-r--r--yt/yt/core/actions/invoker_detail.h55
-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.cpp193
-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/bind_ut.cpp1158
-rw-r--r--yt/yt/core/actions/unittests/future_ut.cpp1634
-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.make47
-rw-r--r--yt/yt/core/bus/bus.h172
-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.h62
-rw-r--r--yt/yt/core/bus/server.h40
-rw-r--r--yt/yt/core/bus/tcp/client.cpp229
-rw-r--r--yt/yt/core/bus/tcp/client.h18
-rw-r--r--yt/yt/core/bus/tcp/config.cpp160
-rw-r--r--yt/yt/core/bus/tcp/config.h150
-rw-r--r--yt/yt/core/bus/tcp/connection.cpp2044
-rw-r--r--yt/yt/core/bus/tcp/connection.h385
-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.cpp326
-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.h85
-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.cpp494
-rw-r--r--yt/yt/core/bus/tcp/server.h17
-rw-r--r--yt/yt/core/bus/tcp/ssl_context.cpp294
-rw-r--r--yt/yt/core/bus/tcp/ssl_context.h45
-rw-r--r--yt/yt/core/bus/unittests/bus_ut.cpp367
-rw-r--r--yt/yt/core/bus/unittests/ssl_ut.cpp547
-rw-r--r--yt/yt/core/bus/unittests/ya.make42
-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.cpp591
-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.cpp613
-rw-r--r--yt/yt/core/concurrency/invoker_queue.h219
-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.cpp1194
-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.h68
-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.cpp124
-rw-r--r--yt/yt/core/concurrency/scheduler_thread.h64
-rw-r--r--yt/yt/core/concurrency/single_queue_scheduler_thread.cpp155
-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.cpp742
-rw-r--r--yt/yt/core/concurrency/two_level_fair_share_thread_pool.h46
-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.cpp231
-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.cpp1621
-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.make62
-rw-r--r--yt/yt/core/crypto/CMakeLists.darwin-x86_64.txt26
-rw-r--r--yt/yt/core/crypto/CMakeLists.linux-aarch64.txt27
-rw-r--r--yt/yt/core/crypto/CMakeLists.linux-x86_64.txt27
-rw-r--r--yt/yt/core/crypto/CMakeLists.txt17
-rw-r--r--yt/yt/core/crypto/CMakeLists.windows-x86_64.txt23
-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.cpp524
-rw-r--r--yt/yt/core/dns/ares_dns_resolver.h14
-rw-r--r--yt/yt/core/dns/config.cpp25
-rw-r--r--yt/yt/core/dns/config.h45
-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.h17
-rw-r--r--yt/yt/core/http/CMakeLists.darwin-x86_64.txt29
-rw-r--r--yt/yt/core/http/CMakeLists.linux-aarch64.txt30
-rw-r--r--yt/yt/core/http/CMakeLists.linux-x86_64.txt30
-rw-r--r--yt/yt/core/http/CMakeLists.txt17
-rw-r--r--yt/yt/core/http/CMakeLists.windows-x86_64.txt26
-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/CMakeLists.darwin-x86_64.txt26
-rw-r--r--yt/yt/core/https/CMakeLists.linux-aarch64.txt27
-rw-r--r--yt/yt/core/https/CMakeLists.linux-x86_64.txt27
-rw-r--r--yt/yt/core/https/CMakeLists.txt17
-rw-r--r--yt/yt/core/https/CMakeLists.windows-x86_64.txt23
-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.cpp508
-rw-r--r--yt/yt/core/logging/config.h238
-rw-r--r--yt/yt/core/logging/file_log_writer.cpp354
-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.cpp218
-rw-r--r--yt/yt/core/logging/formatter.h73
-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.cpp1603
-rw-r--r--yt/yt/core/logging/log_manager.h116
-rw-r--r--yt/yt/core/logging/log_writer.h37
-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/private.h23
-rw-r--r--yt/yt/core/logging/public.h45
-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.cpp1322
-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/CMakeLists.txt9
-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.h354
-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/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.cpp164
-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/CMakeLists.darwin-x86_64.txt30
-rw-r--r--yt/yt/core/misc/isa_crc64/CMakeLists.linux-aarch64.txt18
-rw-r--r--yt/yt/core/misc/isa_crc64/CMakeLists.linux-x86_64.txt31
-rw-r--r--yt/yt/core/misc/isa_crc64/CMakeLists.txt17
-rw-r--r--yt/yt/core/misc/isa_crc64/CMakeLists.windows-x86_64.txt30
-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/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/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.cpp390
-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.make103
-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.cpp1270
-rw-r--r--yt/yt/core/net/address.h279
-rw-r--r--yt/yt/core/net/config.cpp53
-rw-r--r--yt/yt/core/net/config.h65
-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.cpp239
-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.h62
-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.h511
-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.cpp734
-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.cpp583
-rw-r--r--yt/yt/core/rpc/grpc/helpers.h299
-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.cpp28
-rw-r--r--yt/yt/core/rpc/grpc/public.h41
-rw-r--r--yt/yt/core/rpc/grpc/server.cpp1105
-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.cpp373
-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.h192
-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.h1097
-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.make392
-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.h1009
-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.cpp25
-rw-r--r--yt/yt/core/yson/lexer.h25
-rw-r--r--yt/yt/core/yson/lexer_detail.h274
-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.h529
-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.h70
-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.cpp239
-rw-r--r--yt/yt/core/yson/token.h91
-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.cpp47
-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.cpp243
-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.cpp232
-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/lazy_ypath_service_ut.cpp331
-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/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.cpp932
-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.cpp211
-rw-r--r--yt/yt/core/ytree/yson_struct.h311
-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
-rw-r--r--yt/yt/library/CMakeLists.txt14
-rw-r--r--yt/yt/library/numeric/algorithm_helpers-inl.h137
-rw-r--r--yt/yt/library/numeric/algorithm_helpers.h70
-rw-r--r--yt/yt/library/profiling/CMakeLists.darwin-x86_64.txt31
-rw-r--r--yt/yt/library/profiling/CMakeLists.linux-aarch64.txt32
-rw-r--r--yt/yt/library/profiling/CMakeLists.linux-x86_64.txt32
-rw-r--r--yt/yt/library/profiling/CMakeLists.txt17
-rw-r--r--yt/yt/library/profiling/CMakeLists.windows-x86_64.txt28
-rw-r--r--yt/yt/library/profiling/README.md144
-rw-r--r--yt/yt/library/profiling/example/main.cpp162
-rw-r--r--yt/yt/library/profiling/example/ya.make17
-rw-r--r--yt/yt/library/profiling/histogram_snapshot.cpp115
-rw-r--r--yt/yt/library/profiling/histogram_snapshot.h56
-rw-r--r--yt/yt/library/profiling/impl.cpp16
-rw-r--r--yt/yt/library/profiling/impl.h180
-rw-r--r--yt/yt/library/profiling/integration/test_solomon.py103
-rw-r--r--yt/yt/library/profiling/integration/ya.make17
-rw-r--r--yt/yt/library/profiling/perf/counters.cpp250
-rw-r--r--yt/yt/library/profiling/perf/counters.h62
-rw-r--r--yt/yt/library/profiling/perf/counters_other.cpp27
-rw-r--r--yt/yt/library/profiling/perf/ya.make16
-rw-r--r--yt/yt/library/profiling/producer.cpp152
-rw-r--r--yt/yt/library/profiling/producer.h122
-rw-r--r--yt/yt/library/profiling/public.h31
-rw-r--r--yt/yt/library/profiling/resource_tracker/CMakeLists.darwin-x86_64.txt22
-rw-r--r--yt/yt/library/profiling/resource_tracker/CMakeLists.linux-aarch64.txt23
-rw-r--r--yt/yt/library/profiling/resource_tracker/CMakeLists.linux-x86_64.txt23
-rw-r--r--yt/yt/library/profiling/resource_tracker/CMakeLists.txt17
-rw-r--r--yt/yt/library/profiling/resource_tracker/CMakeLists.windows-x86_64.txt19
-rw-r--r--yt/yt/library/profiling/resource_tracker/resource_tracker.cpp444
-rw-r--r--yt/yt/library/profiling/resource_tracker/resource_tracker.h132
-rw-r--r--yt/yt/library/profiling/resource_tracker/ya.make16
-rw-r--r--yt/yt/library/profiling/sensor.cpp659
-rw-r--r--yt/yt/library/profiling/sensor.h408
-rw-r--r--yt/yt/library/profiling/solomon/cube.cpp724
-rw-r--r--yt/yt/library/profiling/solomon/cube.h132
-rw-r--r--yt/yt/library/profiling/solomon/exporter.cpp1005
-rw-r--r--yt/yt/library/profiling/solomon/exporter.h219
-rw-r--r--yt/yt/library/profiling/solomon/percpu.cpp133
-rw-r--r--yt/yt/library/profiling/solomon/percpu.h120
-rw-r--r--yt/yt/library/profiling/solomon/private.h13
-rw-r--r--yt/yt/library/profiling/solomon/producer.cpp216
-rw-r--r--yt/yt/library/profiling/solomon/producer.h96
-rw-r--r--yt/yt/library/profiling/solomon/public.h16
-rw-r--r--yt/yt/library/profiling/solomon/registry.cpp544
-rw-r--r--yt/yt/library/profiling/solomon/registry.h189
-rw-r--r--yt/yt/library/profiling/solomon/remote.cpp212
-rw-r--r--yt/yt/library/profiling/solomon/remote.h54
-rw-r--r--yt/yt/library/profiling/solomon/sensor.cpp268
-rw-r--r--yt/yt/library/profiling/solomon/sensor.h135
-rw-r--r--yt/yt/library/profiling/solomon/sensor_dump.proto57
-rw-r--r--yt/yt/library/profiling/solomon/sensor_service.cpp283
-rw-r--r--yt/yt/library/profiling/solomon/sensor_service.h18
-rw-r--r--yt/yt/library/profiling/solomon/sensor_set.cpp416
-rw-r--r--yt/yt/library/profiling/solomon/sensor_set.h272
-rw-r--r--yt/yt/library/profiling/solomon/tag_registry.cpp118
-rw-r--r--yt/yt/library/profiling/solomon/tag_registry.h62
-rw-r--r--yt/yt/library/profiling/solomon/ya.make33
-rw-r--r--yt/yt/library/profiling/summary-inl.h108
-rw-r--r--yt/yt/library/profiling/summary.h40
-rw-r--r--yt/yt/library/profiling/tag-inl.h138
-rw-r--r--yt/yt/library/profiling/tag.cpp217
-rw-r--r--yt/yt/library/profiling/tag.h141
-rw-r--r--yt/yt/library/profiling/tcmalloc/profiler.cpp64
-rw-r--r--yt/yt/library/profiling/tcmalloc/profiler.h11
-rw-r--r--yt/yt/library/profiling/tcmalloc/ya.make14
-rw-r--r--yt/yt/library/profiling/testing.cpp41
-rw-r--r--yt/yt/library/profiling/testing.h21
-rw-r--r--yt/yt/library/profiling/unittests/cube_ut.cpp65
-rw-r--r--yt/yt/library/profiling/unittests/deps/main.cpp18
-rw-r--r--yt/yt/library/profiling/unittests/deps/ya.make11
-rw-r--r--yt/yt/library/profiling/unittests/exporter_ut.cpp222
-rw-r--r--yt/yt/library/profiling/unittests/name_conflicts_ut.cpp6
-rw-r--r--yt/yt/library/profiling/unittests/perf_counter_ut.cpp63
-rw-r--r--yt/yt/library/profiling/unittests/profiler_ut.cpp84
-rw-r--r--yt/yt/library/profiling/unittests/sensor_ut.cpp31
-rw-r--r--yt/yt/library/profiling/unittests/solomon_ut.cpp878
-rw-r--r--yt/yt/library/profiling/unittests/tag_ut.cpp126
-rw-r--r--yt/yt/library/profiling/unittests/ya.make35
-rw-r--r--yt/yt/library/profiling/ya.make36
-rw-r--r--yt/yt/library/syncmap/CMakeLists.darwin-x86_64.txt14
-rw-r--r--yt/yt/library/syncmap/CMakeLists.linux-aarch64.txt15
-rw-r--r--yt/yt/library/syncmap/CMakeLists.linux-x86_64.txt15
-rw-r--r--yt/yt/library/syncmap/CMakeLists.txt17
-rw-r--r--yt/yt/library/syncmap/CMakeLists.windows-x86_64.txt14
-rw-r--r--yt/yt/library/syncmap/map-inl.h208
-rw-r--r--yt/yt/library/syncmap/map.h115
-rw-r--r--yt/yt/library/syncmap/unittests/map_ut.cpp98
-rw-r--r--yt/yt/library/syncmap/unittests/ya.make14
-rw-r--r--yt/yt/library/syncmap/ya.make15
-rw-r--r--yt/yt/library/tracing/CMakeLists.darwin-x86_64.txt25
-rw-r--r--yt/yt/library/tracing/CMakeLists.linux-aarch64.txt26
-rw-r--r--yt/yt/library/tracing/CMakeLists.linux-x86_64.txt26
-rw-r--r--yt/yt/library/tracing/CMakeLists.txt17
-rw-r--r--yt/yt/library/tracing/CMakeLists.windows-x86_64.txt22
-rw-r--r--yt/yt/library/tracing/README.md7
-rw-r--r--yt/yt/library/tracing/async_queue_trace.cpp134
-rw-r--r--yt/yt/library/tracing/async_queue_trace.h82
-rw-r--r--yt/yt/library/tracing/batch_trace.cpp44
-rw-r--r--yt/yt/library/tracing/batch_trace.h30
-rw-r--r--yt/yt/library/tracing/example/main.cpp93
-rw-r--r--yt/yt/library/tracing/example/ya.make11
-rw-r--r--yt/yt/library/tracing/jaeger/model.proto79
-rw-r--r--yt/yt/library/tracing/jaeger/public.h17
-rw-r--r--yt/yt/library/tracing/jaeger/sampler.cpp125
-rw-r--r--yt/yt/library/tracing/jaeger/sampler.h79
-rw-r--r--yt/yt/library/tracing/jaeger/tracer.cpp616
-rw-r--r--yt/yt/library/tracing/jaeger/tracer.h193
-rw-r--r--yt/yt/library/tracing/jaeger/ya.make16
-rw-r--r--yt/yt/library/tracing/public.cpp1
-rw-r--r--yt/yt/library/tracing/public.h16
-rw-r--r--yt/yt/library/tracing/tracer.cpp10
-rw-r--r--yt/yt/library/tracing/tracer.h24
-rw-r--r--yt/yt/library/tracing/unittests/sampler_ut.cpp39
-rw-r--r--yt/yt/library/tracing/unittests/ya.make13
-rw-r--r--yt/yt/library/tracing/ya.make34
-rw-r--r--yt/yt/library/tvm/CMakeLists.darwin-x86_64.txt21
-rw-r--r--yt/yt/library/tvm/CMakeLists.linux-aarch64.txt22
-rw-r--r--yt/yt/library/tvm/CMakeLists.linux-x86_64.txt22
-rw-r--r--yt/yt/library/tvm/CMakeLists.txt17
-rw-r--r--yt/yt/library/tvm/CMakeLists.windows-x86_64.txt18
-rw-r--r--yt/yt/library/tvm/public.h14
-rw-r--r--yt/yt/library/tvm/tvm_base.cpp18
-rw-r--r--yt/yt/library/tvm/tvm_base.h37
-rw-r--r--yt/yt/library/tvm/ya.make23
-rw-r--r--yt/yt/library/undumpable/CMakeLists.darwin-x86_64.txt25
-rw-r--r--yt/yt/library/undumpable/CMakeLists.linux-aarch64.txt26
-rw-r--r--yt/yt/library/undumpable/CMakeLists.linux-x86_64.txt26
-rw-r--r--yt/yt/library/undumpable/CMakeLists.txt17
-rw-r--r--yt/yt/library/undumpable/CMakeLists.windows-x86_64.txt22
-rw-r--r--yt/yt/library/undumpable/README.md9
-rw-r--r--yt/yt/library/undumpable/ref.cpp49
-rw-r--r--yt/yt/library/undumpable/ref.h13
-rw-r--r--yt/yt/library/undumpable/undumpable.cpp225
-rw-r--r--yt/yt/library/undumpable/undumpable.h56
-rw-r--r--yt/yt/library/undumpable/unittests/undumpable_ut.cpp61
-rw-r--r--yt/yt/library/undumpable/unittests/ya.make19
-rw-r--r--yt/yt/library/undumpable/ya.make21
-rw-r--r--yt/yt/library/ytprof/CMakeLists.txt9
-rw-r--r--yt/yt/library/ytprof/api/CMakeLists.darwin-x86_64.txt21
-rw-r--r--yt/yt/library/ytprof/api/CMakeLists.linux-aarch64.txt22
-rw-r--r--yt/yt/library/ytprof/api/CMakeLists.linux-x86_64.txt22
-rw-r--r--yt/yt/library/ytprof/api/CMakeLists.txt17
-rw-r--r--yt/yt/library/ytprof/api/CMakeLists.windows-x86_64.txt18
-rw-r--r--yt/yt/library/ytprof/api/api.cpp86
-rw-r--r--yt/yt/library/ytprof/api/api.h69
-rw-r--r--yt/yt/library/ytprof/api/atomic_signal_ptr.h59
-rw-r--r--yt/yt/library/ytprof/api/ya.make14
-rw-r--r--yt/yt/ya_check_dependencies.inc15
-rw-r--r--yt/yt_proto/CMakeLists.txt9
-rw-r--r--yt/yt_proto/yt/CMakeLists.txt10
-rw-r--r--yt/yt_proto/yt/core/CMakeLists.darwin-x86_64.txt190
-rw-r--r--yt/yt_proto/yt/core/CMakeLists.linux-aarch64.txt191
-rw-r--r--yt/yt_proto/yt/core/CMakeLists.linux-x86_64.txt191
-rw-r--r--yt/yt_proto/yt/core/CMakeLists.txt17
-rw-r--r--yt/yt_proto/yt/core/CMakeLists.windows-x86_64.txt190
-rw-r--r--yt/yt_proto/yt/core/bus/proto/bus.proto13
-rw-r--r--yt/yt_proto/yt/core/crypto/proto/crypto.proto11
-rw-r--r--yt/yt_proto/yt/core/misc/proto/bloom_filter.proto10
-rw-r--r--yt/yt_proto/yt/core/misc/proto/error.proto20
-rw-r--r--yt/yt_proto/yt/core/misc/proto/guid.proto17
-rw-r--r--yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto28
-rw-r--r--yt/yt_proto/yt/core/rpc/proto/rpc.proto216
-rw-r--r--yt/yt_proto/yt/core/tracing/proto/span.proto38
-rw-r--r--yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto22
-rw-r--r--yt/yt_proto/yt/core/ya.make32
-rw-r--r--yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto30
-rw-r--r--yt/yt_proto/yt/core/ytree/proto/attributes.proto30
-rw-r--r--yt/yt_proto/yt/core/ytree/proto/ypath.proto158
-rw-r--r--yt/yt_proto/yt/formats/CMakeLists.darwin-x86_64.txt56
-rw-r--r--yt/yt_proto/yt/formats/CMakeLists.linux-aarch64.txt57
-rw-r--r--yt/yt_proto/yt/formats/CMakeLists.linux-x86_64.txt57
-rw-r--r--yt/yt_proto/yt/formats/CMakeLists.txt17
-rw-r--r--yt/yt_proto/yt/formats/CMakeLists.windows-x86_64.txt56
-rw-r--r--yt/yt_proto/yt/formats/extension.proto107
-rw-r--r--yt/yt_proto/yt/formats/ya.make14
-rw-r--r--yt/yt_proto/yt/formats/yamr.proto16
2127 files changed, 377412 insertions, 4 deletions
diff --git a/CMakeLists.darwin-x86_64.txt b/CMakeLists.darwin-x86_64.txt
index 73f5b171fc..075d57df50 100644
--- a/CMakeLists.darwin-x86_64.txt
+++ b/CMakeLists.darwin-x86_64.txt
@@ -10,5 +10,6 @@ add_subdirectory(tools)
add_subdirectory(contrib)
add_subdirectory(library)
add_subdirectory(util)
-add_subdirectory(ydb)
+add_subdirectory(yt)
add_subdirectory(certs)
+add_subdirectory(ydb)
diff --git a/CMakeLists.linux-aarch64.txt b/CMakeLists.linux-aarch64.txt
index 73f5b171fc..075d57df50 100644
--- a/CMakeLists.linux-aarch64.txt
+++ b/CMakeLists.linux-aarch64.txt
@@ -10,5 +10,6 @@ add_subdirectory(tools)
add_subdirectory(contrib)
add_subdirectory(library)
add_subdirectory(util)
-add_subdirectory(ydb)
+add_subdirectory(yt)
add_subdirectory(certs)
+add_subdirectory(ydb)
diff --git a/CMakeLists.linux-x86_64.txt b/CMakeLists.linux-x86_64.txt
index 73f5b171fc..075d57df50 100644
--- a/CMakeLists.linux-x86_64.txt
+++ b/CMakeLists.linux-x86_64.txt
@@ -10,5 +10,6 @@ add_subdirectory(tools)
add_subdirectory(contrib)
add_subdirectory(library)
add_subdirectory(util)
-add_subdirectory(ydb)
+add_subdirectory(yt)
add_subdirectory(certs)
+add_subdirectory(ydb)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 38f0ec301e..aef36b675f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,6 +33,7 @@ include(cmake/conan.cmake)
include(cmake/global_flags.cmake)
include(cmake/llvm-tools.cmake)
include(cmake/protobuf.cmake)
+include(cmake/shared_libs.cmake)
include(cmake/global_vars.cmake)
if (CMAKE_CROSSCOMPILING)
diff --git a/CMakeLists.windows-x86_64.txt b/CMakeLists.windows-x86_64.txt
index fa276972cd..e8667d4e27 100644
--- a/CMakeLists.windows-x86_64.txt
+++ b/CMakeLists.windows-x86_64.txt
@@ -10,5 +10,6 @@ add_subdirectory(tools)
add_subdirectory(contrib)
add_subdirectory(util)
add_subdirectory(library)
-add_subdirectory(ydb)
+add_subdirectory(yt)
add_subdirectory(certs)
+add_subdirectory(ydb)
diff --git a/cmake/shared_libs.cmake b/cmake/shared_libs.cmake
new file mode 100644
index 0000000000..2c9de143cc
--- /dev/null
+++ b/cmake/shared_libs.cmake
@@ -0,0 +1,9 @@
+add_custom_target(all-shared-libs)
+
+function(add_shared_library Tgt)
+ add_library(${Tgt} SHARED ${ARGN})
+ add_dependencies(all-shared-libs ${Tgt})
+ if (NOT CMAKE_POSITION_INDEPENDENT_CODE)
+ set_property(TARGET ${Tgt} PROPERTY EXCLUDE_FROM_ALL On)
+ endif()
+endfunction()
diff --git a/contrib/libs/CMakeLists.darwin-x86_64.txt b/contrib/libs/CMakeLists.darwin-x86_64.txt
index faa28913b3..c0a9e6ecad 100644
--- a/contrib/libs/CMakeLists.darwin-x86_64.txt
+++ b/contrib/libs/CMakeLists.darwin-x86_64.txt
@@ -60,6 +60,7 @@ add_subdirectory(tbb)
add_subdirectory(tcmalloc)
add_subdirectory(utf8proc)
add_subdirectory(xxhash)
+add_subdirectory(yajl)
add_subdirectory(yaml-cpp)
add_subdirectory(zstd)
add_subdirectory(zstd06)
diff --git a/contrib/libs/CMakeLists.linux-aarch64.txt b/contrib/libs/CMakeLists.linux-aarch64.txt
index 42d45c16a3..824ae3431f 100644
--- a/contrib/libs/CMakeLists.linux-aarch64.txt
+++ b/contrib/libs/CMakeLists.linux-aarch64.txt
@@ -60,6 +60,7 @@ add_subdirectory(tbb)
add_subdirectory(tcmalloc)
add_subdirectory(utf8proc)
add_subdirectory(xxhash)
+add_subdirectory(yajl)
add_subdirectory(yaml-cpp)
add_subdirectory(zstd)
add_subdirectory(zstd06)
diff --git a/contrib/libs/CMakeLists.linux-x86_64.txt b/contrib/libs/CMakeLists.linux-x86_64.txt
index 37bac16209..e1a97af442 100644
--- a/contrib/libs/CMakeLists.linux-x86_64.txt
+++ b/contrib/libs/CMakeLists.linux-x86_64.txt
@@ -61,6 +61,7 @@ add_subdirectory(tbb)
add_subdirectory(tcmalloc)
add_subdirectory(utf8proc)
add_subdirectory(xxhash)
+add_subdirectory(yajl)
add_subdirectory(yaml-cpp)
add_subdirectory(zstd)
add_subdirectory(zstd06)
diff --git a/contrib/libs/CMakeLists.windows-x86_64.txt b/contrib/libs/CMakeLists.windows-x86_64.txt
index 910254ddc7..f4c4028e47 100644
--- a/contrib/libs/CMakeLists.windows-x86_64.txt
+++ b/contrib/libs/CMakeLists.windows-x86_64.txt
@@ -55,6 +55,7 @@ add_subdirectory(tbb)
add_subdirectory(tcmalloc)
add_subdirectory(utf8proc)
add_subdirectory(xxhash)
+add_subdirectory(yajl)
add_subdirectory(yaml-cpp)
add_subdirectory(zstd)
add_subdirectory(zstd06)
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/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/yajl/CMakeLists.darwin-x86_64.txt b/contrib/libs/yajl/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..41b5ae1274
--- /dev/null
+++ b/contrib/libs/yajl/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,29 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-libs-yajl)
+target_compile_options(contrib-libs-yajl PRIVATE
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_link_libraries(contrib-libs-yajl PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(contrib-libs-yajl PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_buf.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_gen.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_version.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_alloc.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_encode.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_lex.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_tree.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.cpp
+)
diff --git a/contrib/libs/yajl/CMakeLists.linux-aarch64.txt b/contrib/libs/yajl/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..049564bfc9
--- /dev/null
+++ b/contrib/libs/yajl/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,30 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-libs-yajl)
+target_compile_options(contrib-libs-yajl PRIVATE
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_link_libraries(contrib-libs-yajl PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(contrib-libs-yajl PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_buf.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_gen.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_version.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_alloc.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_encode.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_lex.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_tree.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.cpp
+)
diff --git a/contrib/libs/yajl/CMakeLists.linux-x86_64.txt b/contrib/libs/yajl/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..049564bfc9
--- /dev/null
+++ b/contrib/libs/yajl/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,30 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-libs-yajl)
+target_compile_options(contrib-libs-yajl PRIVATE
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_link_libraries(contrib-libs-yajl PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(contrib-libs-yajl PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_buf.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_gen.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_version.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_alloc.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_encode.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_lex.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_tree.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.cpp
+)
diff --git a/contrib/libs/yajl/CMakeLists.txt b/contrib/libs/yajl/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/contrib/libs/yajl/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/contrib/libs/yajl/CMakeLists.windows-x86_64.txt b/contrib/libs/yajl/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..41b5ae1274
--- /dev/null
+++ b/contrib/libs/yajl/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,29 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-libs-yajl)
+target_compile_options(contrib-libs-yajl PRIVATE
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_link_libraries(contrib-libs-yajl PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(contrib-libs-yajl PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_buf.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_gen.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_version.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_alloc.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_encode.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_lex.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_tree.c
+ ${CMAKE_SOURCE_DIR}/contrib/libs/yajl/yajl_parser.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/contrib/restricted/CMakeLists.darwin-x86_64.txt
index f83a626e83..1b481947a0 100644
--- a/contrib/restricted/CMakeLists.darwin-x86_64.txt
+++ b/contrib/restricted/CMakeLists.darwin-x86_64.txt
@@ -15,6 +15,7 @@ add_subdirectory(dragonbox)
add_subdirectory(fast_float)
add_subdirectory(google)
add_subdirectory(googletest)
+add_subdirectory(http-parser)
add_subdirectory(nlohmann_json)
add_subdirectory(thrift)
add_subdirectory(uriparser)
diff --git a/contrib/restricted/CMakeLists.linux-aarch64.txt b/contrib/restricted/CMakeLists.linux-aarch64.txt
index f83a626e83..1b481947a0 100644
--- a/contrib/restricted/CMakeLists.linux-aarch64.txt
+++ b/contrib/restricted/CMakeLists.linux-aarch64.txt
@@ -15,6 +15,7 @@ add_subdirectory(dragonbox)
add_subdirectory(fast_float)
add_subdirectory(google)
add_subdirectory(googletest)
+add_subdirectory(http-parser)
add_subdirectory(nlohmann_json)
add_subdirectory(thrift)
add_subdirectory(uriparser)
diff --git a/contrib/restricted/CMakeLists.linux-x86_64.txt b/contrib/restricted/CMakeLists.linux-x86_64.txt
index f83a626e83..1b481947a0 100644
--- a/contrib/restricted/CMakeLists.linux-x86_64.txt
+++ b/contrib/restricted/CMakeLists.linux-x86_64.txt
@@ -15,6 +15,7 @@ add_subdirectory(dragonbox)
add_subdirectory(fast_float)
add_subdirectory(google)
add_subdirectory(googletest)
+add_subdirectory(http-parser)
add_subdirectory(nlohmann_json)
add_subdirectory(thrift)
add_subdirectory(uriparser)
diff --git a/contrib/restricted/CMakeLists.windows-x86_64.txt b/contrib/restricted/CMakeLists.windows-x86_64.txt
index 92289c85fb..6f1b871db7 100644
--- a/contrib/restricted/CMakeLists.windows-x86_64.txt
+++ b/contrib/restricted/CMakeLists.windows-x86_64.txt
@@ -14,6 +14,7 @@ add_subdirectory(cityhash-1.0.2)
add_subdirectory(fast_float)
add_subdirectory(google)
add_subdirectory(googletest)
+add_subdirectory(http-parser)
add_subdirectory(nlohmann_json)
add_subdirectory(thrift)
add_subdirectory(uriparser)
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/CMakeLists.darwin-x86_64.txt b/contrib/restricted/http-parser/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..1e88ed56de
--- /dev/null
+++ b/contrib/restricted/http-parser/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-restricted-http-parser)
+target_compile_options(contrib-restricted-http-parser PRIVATE
+ -DHTTP_MAX_HEADER_SIZE=0x7fffffff
+ -DHTTP_PARSER_STRICT=0
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_include_directories(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser
+)
+target_sources(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser/http_parser.c
+)
diff --git a/contrib/restricted/http-parser/CMakeLists.linux-aarch64.txt b/contrib/restricted/http-parser/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..89136d341b
--- /dev/null
+++ b/contrib/restricted/http-parser/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-restricted-http-parser)
+target_compile_options(contrib-restricted-http-parser PRIVATE
+ -DHTTP_MAX_HEADER_SIZE=0x7fffffff
+ -DHTTP_PARSER_STRICT=0
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_include_directories(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser
+)
+target_link_libraries(contrib-restricted-http-parser PUBLIC
+ contrib-libs-linux-headers
+)
+target_sources(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser/http_parser.c
+)
diff --git a/contrib/restricted/http-parser/CMakeLists.linux-x86_64.txt b/contrib/restricted/http-parser/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..89136d341b
--- /dev/null
+++ b/contrib/restricted/http-parser/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-restricted-http-parser)
+target_compile_options(contrib-restricted-http-parser PRIVATE
+ -DHTTP_MAX_HEADER_SIZE=0x7fffffff
+ -DHTTP_PARSER_STRICT=0
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_include_directories(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser
+)
+target_link_libraries(contrib-restricted-http-parser PUBLIC
+ contrib-libs-linux-headers
+)
+target_sources(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser/http_parser.c
+)
diff --git a/contrib/restricted/http-parser/CMakeLists.txt b/contrib/restricted/http-parser/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/contrib/restricted/http-parser/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/contrib/restricted/http-parser/CMakeLists.windows-x86_64.txt b/contrib/restricted/http-parser/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..1e88ed56de
--- /dev/null
+++ b/contrib/restricted/http-parser/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(contrib-restricted-http-parser)
+target_compile_options(contrib-restricted-http-parser PRIVATE
+ -DHTTP_MAX_HEADER_SIZE=0x7fffffff
+ -DHTTP_PARSER_STRICT=0
+ $<IF:$<CXX_COMPILER_ID:MSVC>,,-Wno-everything>
+)
+target_include_directories(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser
+)
+target_sources(contrib-restricted-http-parser PRIVATE
+ ${CMAKE_SOURCE_DIR}/contrib/restricted/http-parser/http_parser.c
+)
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/library/cpp/CMakeLists.darwin-x86_64.txt b/library/cpp/CMakeLists.darwin-x86_64.txt
index 81a8d715c0..772027a342 100644
--- a/library/cpp/CMakeLists.darwin-x86_64.txt
+++ b/library/cpp/CMakeLists.darwin-x86_64.txt
@@ -71,6 +71,7 @@ add_subdirectory(retry)
add_subdirectory(sanitizer)
add_subdirectory(scheme)
add_subdirectory(sighandler)
+add_subdirectory(skiff)
add_subdirectory(sliding_window)
add_subdirectory(sse)
add_subdirectory(streams)
@@ -83,6 +84,7 @@ add_subdirectory(threading)
add_subdirectory(time_provider)
add_subdirectory(timezone_conversion)
add_subdirectory(tld)
+add_subdirectory(type_info)
add_subdirectory(unicode)
add_subdirectory(unified_agent_client)
add_subdirectory(uri)
diff --git a/library/cpp/CMakeLists.linux-aarch64.txt b/library/cpp/CMakeLists.linux-aarch64.txt
index 5e6834dea1..cd50b0e3a4 100644
--- a/library/cpp/CMakeLists.linux-aarch64.txt
+++ b/library/cpp/CMakeLists.linux-aarch64.txt
@@ -70,6 +70,7 @@ add_subdirectory(retry)
add_subdirectory(sanitizer)
add_subdirectory(scheme)
add_subdirectory(sighandler)
+add_subdirectory(skiff)
add_subdirectory(sliding_window)
add_subdirectory(sse)
add_subdirectory(streams)
@@ -82,6 +83,7 @@ add_subdirectory(threading)
add_subdirectory(time_provider)
add_subdirectory(timezone_conversion)
add_subdirectory(tld)
+add_subdirectory(type_info)
add_subdirectory(unicode)
add_subdirectory(unified_agent_client)
add_subdirectory(uri)
diff --git a/library/cpp/CMakeLists.linux-x86_64.txt b/library/cpp/CMakeLists.linux-x86_64.txt
index 81a8d715c0..772027a342 100644
--- a/library/cpp/CMakeLists.linux-x86_64.txt
+++ b/library/cpp/CMakeLists.linux-x86_64.txt
@@ -71,6 +71,7 @@ add_subdirectory(retry)
add_subdirectory(sanitizer)
add_subdirectory(scheme)
add_subdirectory(sighandler)
+add_subdirectory(skiff)
add_subdirectory(sliding_window)
add_subdirectory(sse)
add_subdirectory(streams)
@@ -83,6 +84,7 @@ add_subdirectory(threading)
add_subdirectory(time_provider)
add_subdirectory(timezone_conversion)
add_subdirectory(tld)
+add_subdirectory(type_info)
add_subdirectory(unicode)
add_subdirectory(unified_agent_client)
add_subdirectory(uri)
diff --git a/library/cpp/CMakeLists.windows-x86_64.txt b/library/cpp/CMakeLists.windows-x86_64.txt
index 81a8d715c0..772027a342 100644
--- a/library/cpp/CMakeLists.windows-x86_64.txt
+++ b/library/cpp/CMakeLists.windows-x86_64.txt
@@ -71,6 +71,7 @@ add_subdirectory(retry)
add_subdirectory(sanitizer)
add_subdirectory(scheme)
add_subdirectory(sighandler)
+add_subdirectory(skiff)
add_subdirectory(sliding_window)
add_subdirectory(sse)
add_subdirectory(streams)
@@ -83,6 +84,7 @@ add_subdirectory(threading)
add_subdirectory(time_provider)
add_subdirectory(timezone_conversion)
add_subdirectory(tld)
+add_subdirectory(type_info)
add_subdirectory(unicode)
add_subdirectory(unified_agent_client)
add_subdirectory(uri)
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/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/skiff/CMakeLists.darwin-x86_64.txt b/library/cpp/skiff/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..d8d296df29
--- /dev/null
+++ b/library/cpp/skiff/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-skiff)
+target_link_libraries(library-cpp-skiff PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(library-cpp-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_schema.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_validator.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/zerocopy_output_writer.cpp
+)
+generate_enum_serilization(library-cpp-skiff
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/public.h
+ INCLUDE_HEADERS
+ library/cpp/skiff/public.h
+)
diff --git a/library/cpp/skiff/CMakeLists.linux-aarch64.txt b/library/cpp/skiff/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..b0656a7bbe
--- /dev/null
+++ b/library/cpp/skiff/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,33 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-skiff)
+target_link_libraries(library-cpp-skiff PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(library-cpp-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_schema.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_validator.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/zerocopy_output_writer.cpp
+)
+generate_enum_serilization(library-cpp-skiff
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/public.h
+ INCLUDE_HEADERS
+ library/cpp/skiff/public.h
+)
diff --git a/library/cpp/skiff/CMakeLists.linux-x86_64.txt b/library/cpp/skiff/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..b0656a7bbe
--- /dev/null
+++ b/library/cpp/skiff/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,33 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-skiff)
+target_link_libraries(library-cpp-skiff PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(library-cpp-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_schema.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_validator.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/zerocopy_output_writer.cpp
+)
+generate_enum_serilization(library-cpp-skiff
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/public.h
+ INCLUDE_HEADERS
+ library/cpp/skiff/public.h
+)
diff --git a/library/cpp/skiff/CMakeLists.txt b/library/cpp/skiff/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/skiff/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/skiff/CMakeLists.windows-x86_64.txt b/library/cpp/skiff/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..d8d296df29
--- /dev/null
+++ b/library/cpp/skiff/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-skiff)
+target_link_libraries(library-cpp-skiff PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(library-cpp-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_schema.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/skiff_validator.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/zerocopy_output_writer.cpp
+)
+generate_enum_serilization(library-cpp-skiff
+ ${CMAKE_SOURCE_DIR}/library/cpp/skiff/public.h
+ INCLUDE_HEADERS
+ library/cpp/skiff/public.h
+)
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/threading/CMakeLists.txt b/library/cpp/threading/CMakeLists.txt
index 681ef6b24e..018e72c5cc 100644
--- a/library/cpp/threading/CMakeLists.txt
+++ b/library/cpp/threading/CMakeLists.txt
@@ -7,7 +7,9 @@
add_subdirectory(atomic)
+add_subdirectory(blocking_queue)
add_subdirectory(chunk_queue)
+add_subdirectory(cron)
add_subdirectory(equeue)
add_subdirectory(future)
add_subdirectory(hot_swap)
@@ -17,3 +19,4 @@ add_subdirectory(poor_man_openmp)
add_subdirectory(queue)
add_subdirectory(skip_list)
add_subdirectory(task_scheduler)
+add_subdirectory(thread_local)
diff --git a/library/cpp/threading/blocking_queue/CMakeLists.darwin-x86_64.txt b/library/cpp/threading/blocking_queue/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..0fcf5ce481
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-blocking_queue)
+target_link_libraries(cpp-threading-blocking_queue PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-threading-blocking_queue PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/blocking_queue/blocking_queue.cpp
+)
diff --git a/library/cpp/threading/blocking_queue/CMakeLists.linux-aarch64.txt b/library/cpp/threading/blocking_queue/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..640f1e9144
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-blocking_queue)
+target_link_libraries(cpp-threading-blocking_queue PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-threading-blocking_queue PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/blocking_queue/blocking_queue.cpp
+)
diff --git a/library/cpp/threading/blocking_queue/CMakeLists.linux-x86_64.txt b/library/cpp/threading/blocking_queue/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..640f1e9144
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-blocking_queue)
+target_link_libraries(cpp-threading-blocking_queue PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-threading-blocking_queue PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/blocking_queue/blocking_queue.cpp
+)
diff --git a/library/cpp/threading/blocking_queue/CMakeLists.txt b/library/cpp/threading/blocking_queue/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/threading/blocking_queue/CMakeLists.windows-x86_64.txt b/library/cpp/threading/blocking_queue/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..0fcf5ce481
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-blocking_queue)
+target_link_libraries(cpp-threading-blocking_queue PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-threading-blocking_queue PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/blocking_queue/blocking_queue.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/library/cpp/threading/cron/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..429d3b0b0d
--- /dev/null
+++ b/library/cpp/threading/cron/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-cron)
+target_link_libraries(cpp-threading-cron PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+)
+target_sources(cpp-threading-cron PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/cron/cron.cpp
+)
diff --git a/library/cpp/threading/cron/CMakeLists.linux-aarch64.txt b/library/cpp/threading/cron/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..d5bb429c63
--- /dev/null
+++ b/library/cpp/threading/cron/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-cron)
+target_link_libraries(cpp-threading-cron PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+)
+target_sources(cpp-threading-cron PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/cron/cron.cpp
+)
diff --git a/library/cpp/threading/cron/CMakeLists.linux-x86_64.txt b/library/cpp/threading/cron/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..d5bb429c63
--- /dev/null
+++ b/library/cpp/threading/cron/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-cron)
+target_link_libraries(cpp-threading-cron PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+)
+target_sources(cpp-threading-cron PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/cron/cron.cpp
+)
diff --git a/library/cpp/threading/cron/CMakeLists.txt b/library/cpp/threading/cron/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/threading/cron/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/threading/cron/CMakeLists.windows-x86_64.txt b/library/cpp/threading/cron/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..429d3b0b0d
--- /dev/null
+++ b/library/cpp/threading/cron/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-threading-cron)
+target_link_libraries(cpp-threading-cron PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+)
+target_sources(cpp-threading-cron PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/cron/cron.cpp
+)
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/threading/thread_local/CMakeLists.darwin-x86_64.txt b/library/cpp/threading/thread_local/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..2a282845d1
--- /dev/null
+++ b/library/cpp/threading/thread_local/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,31 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-threading-thread_local)
+target_link_libraries(cpp-threading-thread_local PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-hot_swap
+ cpp-threading-skip_list
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-threading-thread_local PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.cpp
+)
+generate_enum_serilization(cpp-threading-thread_local
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.h
+ INCLUDE_HEADERS
+ library/cpp/threading/thread_local/thread_local.h
+)
diff --git a/library/cpp/threading/thread_local/CMakeLists.linux-aarch64.txt b/library/cpp/threading/thread_local/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..69ae5b9e2e
--- /dev/null
+++ b/library/cpp/threading/thread_local/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-threading-thread_local)
+target_link_libraries(cpp-threading-thread_local PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-hot_swap
+ cpp-threading-skip_list
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-threading-thread_local PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.cpp
+)
+generate_enum_serilization(cpp-threading-thread_local
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.h
+ INCLUDE_HEADERS
+ library/cpp/threading/thread_local/thread_local.h
+)
diff --git a/library/cpp/threading/thread_local/CMakeLists.linux-x86_64.txt b/library/cpp/threading/thread_local/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..69ae5b9e2e
--- /dev/null
+++ b/library/cpp/threading/thread_local/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-threading-thread_local)
+target_link_libraries(cpp-threading-thread_local PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-hot_swap
+ cpp-threading-skip_list
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-threading-thread_local PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.cpp
+)
+generate_enum_serilization(cpp-threading-thread_local
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.h
+ INCLUDE_HEADERS
+ library/cpp/threading/thread_local/thread_local.h
+)
diff --git a/library/cpp/threading/thread_local/CMakeLists.txt b/library/cpp/threading/thread_local/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/threading/thread_local/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/threading/thread_local/CMakeLists.windows-x86_64.txt b/library/cpp/threading/thread_local/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..2a282845d1
--- /dev/null
+++ b/library/cpp/threading/thread_local/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,31 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-threading-thread_local)
+target_link_libraries(cpp-threading-thread_local PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-hot_swap
+ cpp-threading-skip_list
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-threading-thread_local PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.cpp
+)
+generate_enum_serilization(cpp-threading-thread_local
+ ${CMAKE_SOURCE_DIR}/library/cpp/threading/thread_local/thread_local.h
+ INCLUDE_HEADERS
+ library/cpp/threading/thread_local/thread_local.h
+)
diff --git a/library/cpp/type_info/CMakeLists.darwin-x86_64.txt b/library/cpp/type_info/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..a1fe5a9709
--- /dev/null
+++ b/library/cpp/type_info/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-type_info)
+target_link_libraries(library-cpp-type_info PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+ yson_pull
+)
+target_sources(library-cpp-type_info PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_info.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/builder.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/error.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_complexity.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_equivalence.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_factory.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_io.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.cpp
+)
+generate_enum_serilization(library-cpp-type_info
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.h
+ INCLUDE_HEADERS
+ library/cpp/type_info/type_list.h
+)
diff --git a/library/cpp/type_info/CMakeLists.linux-aarch64.txt b/library/cpp/type_info/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..388946eff8
--- /dev/null
+++ b/library/cpp/type_info/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,39 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-type_info)
+target_link_libraries(library-cpp-type_info PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+ yson_pull
+)
+target_sources(library-cpp-type_info PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_info.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/builder.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/error.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_complexity.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_equivalence.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_factory.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_io.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.cpp
+)
+generate_enum_serilization(library-cpp-type_info
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.h
+ INCLUDE_HEADERS
+ library/cpp/type_info/type_list.h
+)
diff --git a/library/cpp/type_info/CMakeLists.linux-x86_64.txt b/library/cpp/type_info/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..388946eff8
--- /dev/null
+++ b/library/cpp/type_info/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,39 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-type_info)
+target_link_libraries(library-cpp-type_info PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+ yson_pull
+)
+target_sources(library-cpp-type_info PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_info.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/builder.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/error.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_complexity.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_equivalence.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_factory.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_io.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.cpp
+)
+generate_enum_serilization(library-cpp-type_info
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.h
+ INCLUDE_HEADERS
+ library/cpp/type_info/type_list.h
+)
diff --git a/library/cpp/type_info/CMakeLists.txt b/library/cpp/type_info/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/type_info/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/type_info/CMakeLists.windows-x86_64.txt b/library/cpp/type_info/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..a1fe5a9709
--- /dev/null
+++ b/library/cpp/type_info/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(library-cpp-type_info)
+target_link_libraries(library-cpp-type_info PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ tools-enum_parser-enum_serialization_runtime
+ yson_pull
+)
+target_sources(library-cpp-type_info PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_info.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/builder.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/error.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_complexity.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_equivalence.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_factory.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_io.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.cpp
+)
+generate_enum_serilization(library-cpp-type_info
+ ${CMAKE_SOURCE_DIR}/library/cpp/type_info/type_list.h
+ INCLUDE_HEADERS
+ library/cpp/type_info/type_list.h
+)
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/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/README.txt b/library/cpp/type_info/ut/test-data/README.txt
new file mode 100644
index 0000000000..e4ce965357
--- /dev/null
+++ b/library/cpp/type_info/ut/test-data/README.txt
@@ -0,0 +1,5 @@
+The format of the text files is as follows:
+ 1. Each line starting with '#' is comment and must be ignored.
+ 2. Test case are separated with ';;'
+ 3. Each test case has predefined number of fields. Fields are separated with '::'.
+ 4. Meaning of the fields are described in test case.
diff --git a/library/cpp/type_info/ut/test-data/bad-types.txt b/library/cpp/type_info/ut/test-data/bad-types.txt
new file mode 100644
index 0000000000..c8f3120f63
--- /dev/null
+++ b/library/cpp/type_info/ut/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/ut/test-data/good-types.txt b/library/cpp/type_info/ut/test-data/good-types.txt
new file mode 100644
index 0000000000..cb082707b6
--- /dev/null
+++ b/library/cpp/type_info/ut/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/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..6b32a2a152
--- /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/ut/test-data/good-types.txt /good
+ ${ARCADIA_ROOT}/library/cpp/type_info/ut/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/CMakeLists.txt b/library/cpp/yt/CMakeLists.txt
index eb0165800f..db789d02dd 100644
--- a/library/cpp/yt/CMakeLists.txt
+++ b/library/cpp/yt/CMakeLists.txt
@@ -7,12 +7,19 @@
add_subdirectory(assert)
+add_subdirectory(backtrace)
add_subdirectory(coding)
+add_subdirectory(containers)
+add_subdirectory(cpu_clock)
add_subdirectory(exception)
+add_subdirectory(logging)
add_subdirectory(malloc)
add_subdirectory(memory)
add_subdirectory(misc)
add_subdirectory(small_containers)
add_subdirectory(string)
+add_subdirectory(system)
+add_subdirectory(threading)
+add_subdirectory(user_job_statistics)
add_subdirectory(yson)
add_subdirectory(yson_string)
diff --git a/library/cpp/yt/backtrace/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/backtrace/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..dbc5fa609f
--- /dev/null
+++ b/library/cpp/yt/backtrace/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(cursors)
+
+add_library(cpp-yt-backtrace)
+target_compile_options(cpp-yt-backtrace PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-backtrace PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-string
+)
+target_sources(cpp-yt-backtrace PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/backtrace.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/symbolizers/dynload/dynload_symbolizer.cpp
+)
diff --git a/library/cpp/yt/backtrace/CMakeLists.linux-aarch64.txt b/library/cpp/yt/backtrace/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..358ab6a86f
--- /dev/null
+++ b/library/cpp/yt/backtrace/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(cursors)
+
+add_library(cpp-yt-backtrace)
+target_compile_options(cpp-yt-backtrace PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-backtrace PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-string
+)
+target_sources(cpp-yt-backtrace PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/backtrace.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/symbolizers/dynload/dynload_symbolizer.cpp
+)
diff --git a/library/cpp/yt/backtrace/CMakeLists.linux-x86_64.txt b/library/cpp/yt/backtrace/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..358ab6a86f
--- /dev/null
+++ b/library/cpp/yt/backtrace/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(cursors)
+
+add_library(cpp-yt-backtrace)
+target_compile_options(cpp-yt-backtrace PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-backtrace PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-string
+)
+target_sources(cpp-yt-backtrace PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/backtrace.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/symbolizers/dynload/dynload_symbolizer.cpp
+)
diff --git a/library/cpp/yt/backtrace/CMakeLists.txt b/library/cpp/yt/backtrace/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/backtrace/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/backtrace/CMakeLists.windows-x86_64.txt b/library/cpp/yt/backtrace/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..8b4a651f33
--- /dev/null
+++ b/library/cpp/yt/backtrace/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,20 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(cursors)
+
+add_library(cpp-yt-backtrace)
+target_link_libraries(cpp-yt-backtrace PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-string
+)
+target_sources(cpp-yt-backtrace PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/backtrace.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/symbolizers/dummy/dummy_symbolizer.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/backtrace/cursors/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..6c6f5d1c50
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(libunwind)
diff --git a/library/cpp/yt/backtrace/cursors/CMakeLists.linux-aarch64.txt b/library/cpp/yt/backtrace/cursors/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..6c6f5d1c50
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(libunwind)
diff --git a/library/cpp/yt/backtrace/cursors/CMakeLists.linux-x86_64.txt b/library/cpp/yt/backtrace/cursors/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..6c6f5d1c50
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(libunwind)
diff --git a/library/cpp/yt/backtrace/cursors/CMakeLists.txt b/library/cpp/yt/backtrace/cursors/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/backtrace/cursors/CMakeLists.windows-x86_64.txt b/library/cpp/yt/backtrace/cursors/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..961a9a908b
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(dummy)
diff --git a/library/cpp/yt/backtrace/cursors/dummy/CMakeLists.txt b/library/cpp/yt/backtrace/cursors/dummy/CMakeLists.txt
new file mode 100644
index 0000000000..03d4a7153c
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/dummy/CMakeLists.txt
@@ -0,0 +1,11 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/backtrace/cursors/dummy/CMakeLists.windows-x86_64.txt b/library/cpp/yt/backtrace/cursors/dummy/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..40a6e7d0a8
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/dummy/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(backtrace-cursors-dummy)
+target_link_libraries(backtrace-cursors-dummy PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(backtrace-cursors-dummy PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..fdea07f78c
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(backtrace-cursors-libunwind)
+target_compile_options(backtrace-cursors-libunwind PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(backtrace-cursors-libunwind PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-libunwind
+)
+target_sources(backtrace-cursors-libunwind PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.cpp
+)
diff --git a/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-aarch64.txt b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..9d7858cc27
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(backtrace-cursors-libunwind)
+target_compile_options(backtrace-cursors-libunwind PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(backtrace-cursors-libunwind PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-libunwind
+)
+target_sources(backtrace-cursors-libunwind PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.cpp
+)
diff --git a/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-x86_64.txt b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..9d7858cc27
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(backtrace-cursors-libunwind)
+target_compile_options(backtrace-cursors-libunwind PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(backtrace-cursors-libunwind PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-libunwind
+)
+target_sources(backtrace-cursors-libunwind PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.cpp
+)
diff --git a/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.txt b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.txt
new file mode 100644
index 0000000000..606ff46b4b
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/libunwind/CMakeLists.txt
@@ -0,0 +1,15 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
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/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/containers/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..e570a542e4
--- /dev/null
+++ b/library/cpp/yt/containers/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,15 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-containers INTERFACE)
+target_link_libraries(cpp-yt-containers INTERFACE
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
diff --git a/library/cpp/yt/containers/CMakeLists.linux-aarch64.txt b/library/cpp/yt/containers/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..b0ce8a3922
--- /dev/null
+++ b/library/cpp/yt/containers/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,16 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-containers INTERFACE)
+target_link_libraries(cpp-yt-containers INTERFACE
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
diff --git a/library/cpp/yt/containers/CMakeLists.linux-x86_64.txt b/library/cpp/yt/containers/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..b0ce8a3922
--- /dev/null
+++ b/library/cpp/yt/containers/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,16 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-containers INTERFACE)
+target_link_libraries(cpp-yt-containers INTERFACE
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
diff --git a/library/cpp/yt/containers/CMakeLists.txt b/library/cpp/yt/containers/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/containers/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/containers/CMakeLists.windows-x86_64.txt b/library/cpp/yt/containers/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..e570a542e4
--- /dev/null
+++ b/library/cpp/yt/containers/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,15 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-containers INTERFACE)
+target_link_libraries(cpp-yt-containers INTERFACE
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
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/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/cpu_clock/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..b9afea23f7
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-cpu_clock)
+target_compile_options(cpp-yt-cpu_clock PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-cpu_clock PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
+target_sources(cpp-yt-cpu_clock PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/cpu_clock/clock.cpp
+)
diff --git a/library/cpp/yt/cpu_clock/CMakeLists.linux-aarch64.txt b/library/cpp/yt/cpu_clock/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..89fe774bc0
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-cpu_clock)
+target_compile_options(cpp-yt-cpu_clock PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-cpu_clock PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
+target_sources(cpp-yt-cpu_clock PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/cpu_clock/clock.cpp
+)
diff --git a/library/cpp/yt/cpu_clock/CMakeLists.linux-x86_64.txt b/library/cpp/yt/cpu_clock/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..89fe774bc0
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-cpu_clock)
+target_compile_options(cpp-yt-cpu_clock PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-cpu_clock PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
+target_sources(cpp-yt-cpu_clock PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/cpu_clock/clock.cpp
+)
diff --git a/library/cpp/yt/cpu_clock/CMakeLists.txt b/library/cpp/yt/cpu_clock/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/cpu_clock/CMakeLists.windows-x86_64.txt b/library/cpp/yt/cpu_clock/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..cbe906d57f
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-cpu_clock)
+target_link_libraries(cpp-yt-cpu_clock PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+)
+target_sources(cpp-yt-cpu_clock PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/cpu_clock/clock.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/logging/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..b9c4a4c7db
--- /dev/null
+++ b/library/cpp/yt/logging/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(plain_text_formatter)
+
+add_library(cpp-yt-logging)
+target_compile_options(cpp-yt-logging PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-logging PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-memory
+ cpp-yt-misc
+ cpp-yt-yson_string
+)
+target_sources(cpp-yt-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/logger.cpp
+)
diff --git a/library/cpp/yt/logging/CMakeLists.linux-aarch64.txt b/library/cpp/yt/logging/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..bd29994891
--- /dev/null
+++ b/library/cpp/yt/logging/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(plain_text_formatter)
+
+add_library(cpp-yt-logging)
+target_compile_options(cpp-yt-logging PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-logging PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-memory
+ cpp-yt-misc
+ cpp-yt-yson_string
+)
+target_sources(cpp-yt-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/logger.cpp
+)
diff --git a/library/cpp/yt/logging/CMakeLists.linux-x86_64.txt b/library/cpp/yt/logging/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..bd29994891
--- /dev/null
+++ b/library/cpp/yt/logging/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(plain_text_formatter)
+
+add_library(cpp-yt-logging)
+target_compile_options(cpp-yt-logging PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-logging PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-memory
+ cpp-yt-misc
+ cpp-yt-yson_string
+)
+target_sources(cpp-yt-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/logger.cpp
+)
diff --git a/library/cpp/yt/logging/CMakeLists.txt b/library/cpp/yt/logging/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/logging/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/logging/CMakeLists.windows-x86_64.txt b/library/cpp/yt/logging/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..5e766966b0
--- /dev/null
+++ b/library/cpp/yt/logging/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(plain_text_formatter)
+
+add_library(cpp-yt-logging)
+target_link_libraries(cpp-yt-logging PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-memory
+ cpp-yt-misc
+ cpp-yt-yson_string
+)
+target_sources(cpp-yt-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/logger.cpp
+)
diff --git a/library/cpp/yt/logging/backends/arcadia/backend.cpp b/library/cpp/yt/logging/backends/arcadia/backend.cpp
new file mode 100644
index 0000000000..3c6ff9f5f5
--- /dev/null
+++ b/library/cpp/yt/logging/backends/arcadia/backend.cpp
@@ -0,0 +1,86 @@
+#include "backend.h"
+
+#include <library/cpp/logger/backend.h>
+#include <library/cpp/logger/record.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/logging/logger.h>
+
+namespace NYT::NLogging {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ELogLevel ConvertToLogLevel(ELogPriority priority)
+{
+ switch (priority) {
+ case ELogPriority::TLOG_DEBUG:
+ return ELogLevel::Debug;
+ case ELogPriority::TLOG_INFO:
+ [[fallthrough]];
+ case ELogPriority::TLOG_NOTICE:
+ return ELogLevel::Info;
+ case ELogPriority::TLOG_WARNING:
+ return ELogLevel::Warning;
+ case ELogPriority::TLOG_ERR:
+ return ELogLevel::Error;
+ case ELogPriority::TLOG_CRIT:
+ case ELogPriority::TLOG_ALERT:
+ return ELogLevel::Alert;
+ case ELogPriority::TLOG_EMERG:
+ return ELogLevel::Fatal;
+ case ELogPriority::TLOG_RESOURCES:
+ return ELogLevel::Maximum;
+ }
+ YT_ABORT();
+}
+
+class TLogBackendBridge
+ : public TLogBackend
+{
+public:
+ TLogBackendBridge(const TLogger& logger)
+ : Logger_(logger)
+ { }
+
+ void WriteData(const TLogRecord& rec) override
+ {
+ const auto logLevel = ConvertToLogLevel(rec.Priority);
+ if (!Logger_.IsLevelEnabled(logLevel)) {
+ return;
+ }
+
+ // Remove trailing \n, because it will add it.
+ TStringBuf message(rec.Data, rec.Len);
+ message.ChopSuffix(TStringBuf("\n"));
+ // Use low-level api, because it is more convinient here.
+ auto loggingContext = GetLoggingContext();
+ auto event = NDetail::CreateLogEvent(loggingContext, Logger_, logLevel);
+ event.MessageRef = NDetail::BuildLogMessage(loggingContext, Logger_, message).MessageRef;
+ event.Family = ELogFamily::PlainText;
+ Logger_.Write(std::move(event));
+ }
+
+ void ReopenLog() override
+ { }
+
+ ELogPriority FiltrationLevel() const override
+ {
+ return LOG_MAX_PRIORITY;
+ }
+
+private:
+ const TLogger Logger_;
+};
+
+} // namespace
+
+THolder<TLogBackend> CreateArcadiaLogBackend(const TLogger& logger)
+{
+ return MakeHolder<TLogBackendBridge>(logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/backends/arcadia/backend.h b/library/cpp/yt/logging/backends/arcadia/backend.h
new file mode 100644
index 0000000000..251918c972
--- /dev/null
+++ b/library/cpp/yt/logging/backends/arcadia/backend.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <library/cpp/yt/logging/public.h>
+
+#include <util/generic/ptr.h>
+
+class TLogBackend;
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Create TLogBackend which redirects log messages to |logger|.
+THolder<TLogBackend> CreateArcadiaLogBackend(const TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/backends/arcadia/ya.make b/library/cpp/yt/logging/backends/arcadia/ya.make
new file mode 100644
index 0000000000..ee90be8108
--- /dev/null
+++ b/library/cpp/yt/logging/backends/arcadia/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ backend.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/assert
+ library/cpp/yt/logging
+
+ library/cpp/logger
+)
+
+END()
diff --git a/library/cpp/yt/logging/backends/stream/stream_log_manager.cpp b/library/cpp/yt/logging/backends/stream/stream_log_manager.cpp
new file mode 100644
index 0000000000..62269dc0c0
--- /dev/null
+++ b/library/cpp/yt/logging/backends/stream/stream_log_manager.cpp
@@ -0,0 +1,87 @@
+#include "stream_log_manager.h"
+
+#include <library/cpp/yt/logging/logger.h>
+
+#include <library/cpp/yt/logging/plain_text_formatter/formatter.h>
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStreamLogManager
+ : public ILogManager
+{
+public:
+ explicit TStreamLogManager(IOutputStream* output)
+ : Output_(output)
+ { }
+
+ void RegisterStaticAnchor(
+ TLoggingAnchor* anchor,
+ ::TSourceLocation /*sourceLocation*/,
+ TStringBuf /*anchorMessage*/) override
+ {
+ anchor->Registered = true;
+ }
+
+ virtual void UpdateAnchor(TLoggingAnchor* anchor) override
+ {
+ anchor->Enabled = true;
+ }
+
+ virtual void Enqueue(TLogEvent&& event) override
+ {
+ Buffer_.Reset();
+ EventFormatter_.Format(&Buffer_, event);
+ *Output_ << Buffer_.GetBuffer() << Endl;
+ }
+
+ virtual const TLoggingCategory* GetCategory(TStringBuf categoryName) override
+ {
+ 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_;
+ category->CurrentVersion = Version_.load();
+ it = NameToCategory_.emplace(categoryName, std::move(category)).first;
+ }
+ return it->second.get();
+ }
+
+ virtual void UpdateCategory(TLoggingCategory* /*category*/) override
+ { }
+
+ virtual bool GetAbortOnAlert() const override
+ {
+ return false;
+ }
+
+private:
+ IOutputStream* const Output_;
+
+ NThreading::TForkAwareSpinLock SpinLock_;
+ THashMap<TString, std::unique_ptr<TLoggingCategory>> NameToCategory_;
+ std::atomic<int> Version_ = 1;
+
+ TPlainTextEventFormatter EventFormatter_{/*enableSourceLocation*/ false};
+ TRawFormatter<MessageBufferSize> Buffer_;
+};
+
+std::unique_ptr<ILogManager> CreateStreamLogManager(IOutputStream* output)
+{
+ return std::make_unique<TStreamLogManager>(output);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/backends/stream/stream_log_manager.h b/library/cpp/yt/logging/backends/stream/stream_log_manager.h
new file mode 100644
index 0000000000..2f6794e587
--- /dev/null
+++ b/library/cpp/yt/logging/backends/stream/stream_log_manager.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <library/cpp/yt/logging/public.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a dead-simple implementation that synchronously logs
+//! all events to #output.
+std::unique_ptr<ILogManager> CreateStreamLogManager(IOutputStream* output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp b/library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp
new file mode 100644
index 0000000000..cb3e244e3b
--- /dev/null
+++ b/library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp
@@ -0,0 +1,37 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/logging/logger.h>
+
+#include <library/cpp/yt/logging/backends/stream/stream_log_manager.h>
+
+#include <util/stream/str.h>
+
+#include <util/string/split.h>
+
+namespace NYT::NLogging {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TStreamLogManagerTest, Simple)
+{
+ TString str;
+ {
+ TStringOutput output(str);
+ auto logManager = CreateStreamLogManager(&output);
+ TLogger Logger(logManager.get(), "Test");
+ YT_LOG_INFO("Hello world");
+ }
+
+ TVector<TStringBuf> tokens;
+ Split(str, "\t", tokens);
+ EXPECT_GE(std::ssize(tokens), 4);
+ EXPECT_EQ(tokens[1], "I");
+ EXPECT_EQ(tokens[2], "Test");
+ EXPECT_EQ(tokens[3], "Hello world");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/backends/stream/unittests/ya.make b/library/cpp/yt/logging/backends/stream/unittests/ya.make
new file mode 100644
index 0000000000..29270459fa
--- /dev/null
+++ b/library/cpp/yt/logging/backends/stream/unittests/ya.make
@@ -0,0 +1,14 @@
+GTEST(unittester-library-logging)
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ stream_log_manager_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/gtest
+ library/cpp/yt/logging/backends/stream
+)
+
+END()
diff --git a/library/cpp/yt/logging/backends/stream/ya.make b/library/cpp/yt/logging/backends/stream/ya.make
new file mode 100644
index 0000000000..86e3aa046b
--- /dev/null
+++ b/library/cpp/yt/logging/backends/stream/ya.make
@@ -0,0 +1,20 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ stream_log_manager.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/logging
+ library/cpp/yt/logging/plain_text_formatter
+ library/cpp/yt/string
+ library/cpp/yt/threading
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ unittests
+)
diff --git a/library/cpp/yt/logging/backends/ya.make b/library/cpp/yt/logging/backends/ya.make
new file mode 100644
index 0000000000..6ee4b72bfe
--- /dev/null
+++ b/library/cpp/yt/logging/backends/ya.make
@@ -0,0 +1,4 @@
+RECURSE(
+ arcadia
+ stream
+)
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..686ce9251c
--- /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* anchor,
+ ::TSourceLocation sourceLocation,
+ TStringBuf anchorMessage) = 0;
+ virtual void UpdateAnchor(TLoggingAnchor* anchor) = 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/plain_text_formatter/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..3be59e00af
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-logging-plain_text_formatter)
+target_compile_options(yt-logging-plain_text_formatter PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-logging-plain_text_formatter PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-cpu_clock
+ cpp-yt-logging
+ cpp-yt-string
+ cpp-yt-misc
+)
+target_sources(yt-logging-plain_text_formatter PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
+)
diff --git a/library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-aarch64.txt b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..2771fdd74f
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-logging-plain_text_formatter)
+target_compile_options(yt-logging-plain_text_formatter PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-logging-plain_text_formatter PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-cpu_clock
+ cpp-yt-logging
+ cpp-yt-string
+ cpp-yt-misc
+)
+target_sources(yt-logging-plain_text_formatter PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
+)
diff --git a/library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-x86_64.txt b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..2771fdd74f
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-logging-plain_text_formatter)
+target_compile_options(yt-logging-plain_text_formatter PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-logging-plain_text_formatter PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-cpu_clock
+ cpp-yt-logging
+ cpp-yt-string
+ cpp-yt-misc
+)
+target_sources(yt-logging-plain_text_formatter PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
+)
diff --git a/library/cpp/yt/logging/plain_text_formatter/CMakeLists.txt b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/logging/plain_text_formatter/CMakeLists.windows-x86_64.txt b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..d78a3651a0
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-logging-plain_text_formatter)
+target_link_libraries(yt-logging-plain_text_formatter PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-cpu_clock
+ cpp-yt-logging
+ cpp-yt-string
+ cpp-yt-misc
+)
+target_sources(yt-logging-plain_text_formatter PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
+)
diff --git a/library/cpp/yt/logging/plain_text_formatter/formatter.cpp b/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
new file mode 100644
index 0000000000..ab2181113d
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
@@ -0,0 +1,226 @@
+#include "formatter.h"
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+#include <library/cpp/yt/misc/port.h>
+
+#ifdef YT_USE_SSE42
+ #include <emmintrin.h>
+ #include <pmmintrin.h>
+#endif
+
+namespace NYT::NLogging {
+
+constexpr int MessageBufferWatermarkSize = 256;
+
+////////////////////////////////////////////////////////////////////////////////
+
+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()) {
+ if (out->GetBytesRemaining() < MessageBufferWatermarkSize) {
+ out->AppendString(TStringBuf("...<message truncated>"));
+ break;
+ }
+#ifdef YT_USE_SSE42
+ // Use SSE for optimization.
+ if (current + 16 > message.end()) {
+ appendChar();
+ } 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
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+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);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPlainTextEventFormatter::TPlainTextEventFormatter(bool enableSourceLocation)
+ : EnableSourceLocation_(enableSourceLocation)
+{ }
+
+void TPlainTextEventFormatter::Format(TBaseFormatter* buffer, const TLogEvent& event)
+{
+ 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 != TThreadId()) {
+ buffer->AppendNumber(event.ThreadId, 16);
+ }
+
+ buffer->AppendChar('\t');
+
+ if (event.FiberId != TFiberId()) {
+ buffer->AppendNumber(event.FiberId, 16);
+ }
+
+ buffer->AppendChar('\t');
+
+ if (event.TraceId != TTraceId()) {
+ 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');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/plain_text_formatter/formatter.h b/library/cpp/yt/logging/plain_text_formatter/formatter.h
new file mode 100644
index 0000000000..1c35c7c5ee
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/formatter.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <library/cpp/yt/logging//logger.h>
+
+namespace NYT::NLogging {
+
+/////////////////////////////////////////////////////////////π///////////////////
+
+constexpr int DateTimeBufferSize = 64;
+constexpr int MessageBufferSize = 64_KB;
+
+void FormatMilliseconds(TBaseFormatter* out, TInstant dateTime);
+void FormatMicroseconds(TBaseFormatter* out, TInstant dateTime);
+void FormatLevel(TBaseFormatter* out, ELogLevel level);
+void FormatMessage(TBaseFormatter* out, TStringBuf message);
+
+/////////////////////////////////////////////////////////////π///////////////////
+
+class TCachingDateFormatter
+{
+public:
+ void Format(TBaseFormatter* buffer, TInstant dateTime, bool printMicroseconds = false);
+
+private:
+ ui64 CachedSecond_ = 0;
+ TRawFormatter<DateTimeBufferSize> Cached_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPlainTextEventFormatter
+{
+public:
+ explicit TPlainTextEventFormatter(bool enableSourceLocation);
+
+ void Format(TBaseFormatter* buffer, const TLogEvent& event);
+
+private:
+ const bool EnableSourceLocation_;
+
+ TCachingDateFormatter CachingDateFormatter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/plain_text_formatter/ya.make b/library/cpp/yt/logging/plain_text_formatter/ya.make
new file mode 100644
index 0000000000..cb31c29d11
--- /dev/null
+++ b/library/cpp/yt/logging/plain_text_formatter/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ formatter.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/cpu_clock
+ library/cpp/yt/logging
+ library/cpp/yt/string
+ library/cpp/yt/misc
+)
+
+END()
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..e611c2e554
--- /dev/null
+++ b/library/cpp/yt/logging/ya.make
@@ -0,0 +1,25 @@
+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(
+ backends
+ plain_text_formatter
+)
+
+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..eb8264968e
--- /dev/null
+++ b/library/cpp/yt/misc/property.h
@@ -0,0 +1,308 @@
+#pragma once
+
+#include <util/system/compiler.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! 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/system/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/system/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..ad24a0da29
--- /dev/null
+++ b/library/cpp/yt/system/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,20 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-system)
+target_compile_options(cpp-yt-system PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-system PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-yt-system PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/system/thread_id.cpp
+)
diff --git a/library/cpp/yt/system/CMakeLists.linux-aarch64.txt b/library/cpp/yt/system/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..6dc2a07499
--- /dev/null
+++ b/library/cpp/yt/system/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-system)
+target_compile_options(cpp-yt-system PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-system PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-yt-system PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/system/thread_id.cpp
+)
diff --git a/library/cpp/yt/system/CMakeLists.linux-x86_64.txt b/library/cpp/yt/system/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..6dc2a07499
--- /dev/null
+++ b/library/cpp/yt/system/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-system)
+target_compile_options(cpp-yt-system PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-system PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-yt-system PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/system/thread_id.cpp
+)
diff --git a/library/cpp/yt/system/CMakeLists.txt b/library/cpp/yt/system/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/system/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/system/CMakeLists.windows-x86_64.txt b/library/cpp/yt/system/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..338956fa70
--- /dev/null
+++ b/library/cpp/yt/system/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-system)
+target_link_libraries(cpp-yt-system PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(cpp-yt-system PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/system/thread_id.cpp
+)
diff --git a/library/cpp/yt/threading/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/threading/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..7dbacb9da4
--- /dev/null
+++ b/library/cpp/yt/threading/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,37 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-threading)
+target_compile_options(cpp-yt-threading PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-threading PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-system
+ cpp-yt-memory
+)
+target_sources(cpp-yt-threading PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/at_fork.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/count_down_latch.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/event_count.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/futex.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/notification_handle.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/public.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/recursive_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock_base.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait_hook.cpp
+)
diff --git a/library/cpp/yt/threading/CMakeLists.linux-aarch64.txt b/library/cpp/yt/threading/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..644ee262f0
--- /dev/null
+++ b/library/cpp/yt/threading/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-threading)
+target_compile_options(cpp-yt-threading PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-threading PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-system
+ cpp-yt-memory
+)
+target_sources(cpp-yt-threading PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/at_fork.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/count_down_latch.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/event_count.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/futex.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/notification_handle.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/public.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/recursive_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock_base.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait_hook.cpp
+)
diff --git a/library/cpp/yt/threading/CMakeLists.linux-x86_64.txt b/library/cpp/yt/threading/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..644ee262f0
--- /dev/null
+++ b/library/cpp/yt/threading/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-threading)
+target_compile_options(cpp-yt-threading PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-yt-threading PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-system
+ cpp-yt-memory
+)
+target_sources(cpp-yt-threading PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/at_fork.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/count_down_latch.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/event_count.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/futex.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/notification_handle.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/public.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/recursive_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock_base.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait_hook.cpp
+)
diff --git a/library/cpp/yt/threading/CMakeLists.txt b/library/cpp/yt/threading/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/threading/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/threading/CMakeLists.windows-x86_64.txt b/library/cpp/yt/threading/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..eedf3ebee2
--- /dev/null
+++ b/library/cpp/yt/threading/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,34 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-threading)
+target_link_libraries(cpp-yt-threading PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-system
+ cpp-yt-memory
+)
+target_sources(cpp-yt-threading PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/at_fork.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/count_down_latch.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/event_count.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/fork_aware_rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/futex.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/notification_handle.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/public.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/recursive_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/rw_spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock_base.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_lock.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait.cpp
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/threading/spin_wait_hook.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/library/cpp/yt/user_job_statistics/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..6ad2fc33f1
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-user_job_statistics)
+target_link_libraries(cpp-yt-user_job_statistics PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+)
+target_sources(cpp-yt-user_job_statistics PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/user_job_statistics/user_job_statistics.cpp
+)
diff --git a/library/cpp/yt/user_job_statistics/CMakeLists.linux-aarch64.txt b/library/cpp/yt/user_job_statistics/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..33fb6e94cc
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-user_job_statistics)
+target_link_libraries(cpp-yt-user_job_statistics PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+)
+target_sources(cpp-yt-user_job_statistics PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/user_job_statistics/user_job_statistics.cpp
+)
diff --git a/library/cpp/yt/user_job_statistics/CMakeLists.linux-x86_64.txt b/library/cpp/yt/user_job_statistics/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..33fb6e94cc
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-user_job_statistics)
+target_link_libraries(cpp-yt-user_job_statistics PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+)
+target_sources(cpp-yt-user_job_statistics PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/user_job_statistics/user_job_statistics.cpp
+)
diff --git a/library/cpp/yt/user_job_statistics/CMakeLists.txt b/library/cpp/yt/user_job_statistics/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/library/cpp/yt/user_job_statistics/CMakeLists.windows-x86_64.txt b/library/cpp/yt/user_job_statistics/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..6ad2fc33f1
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-yt-user_job_statistics)
+target_link_libraries(cpp-yt-user_job_statistics PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+)
+target_sources(cpp-yt-user_job_statistics PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/yt/user_job_statistics/user_job_statistics.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/core/CMakeLists.darwin-x86_64.txt
index b46783d4f6..6b6010bdea 100644
--- a/ydb/library/yql/core/CMakeLists.darwin-x86_64.txt
+++ b/ydb/library/yql/core/CMakeLists.darwin-x86_64.txt
@@ -20,6 +20,7 @@ add_subdirectory(peephole_opt)
add_subdirectory(services)
add_subdirectory(sql_types)
add_subdirectory(type_ann)
+add_subdirectory(url_preprocessing)
add_subdirectory(user_data)
add_subdirectory(ut)
get_built_tool_path(
diff --git a/ydb/library/yql/core/CMakeLists.linux-aarch64.txt b/ydb/library/yql/core/CMakeLists.linux-aarch64.txt
index 16da3cf008..3d1fb5aec4 100644
--- a/ydb/library/yql/core/CMakeLists.linux-aarch64.txt
+++ b/ydb/library/yql/core/CMakeLists.linux-aarch64.txt
@@ -20,6 +20,7 @@ add_subdirectory(peephole_opt)
add_subdirectory(services)
add_subdirectory(sql_types)
add_subdirectory(type_ann)
+add_subdirectory(url_preprocessing)
add_subdirectory(user_data)
add_subdirectory(ut)
get_built_tool_path(
diff --git a/ydb/library/yql/core/CMakeLists.linux-x86_64.txt b/ydb/library/yql/core/CMakeLists.linux-x86_64.txt
index 16da3cf008..3d1fb5aec4 100644
--- a/ydb/library/yql/core/CMakeLists.linux-x86_64.txt
+++ b/ydb/library/yql/core/CMakeLists.linux-x86_64.txt
@@ -20,6 +20,7 @@ add_subdirectory(peephole_opt)
add_subdirectory(services)
add_subdirectory(sql_types)
add_subdirectory(type_ann)
+add_subdirectory(url_preprocessing)
add_subdirectory(user_data)
add_subdirectory(ut)
get_built_tool_path(
diff --git a/ydb/library/yql/core/CMakeLists.windows-x86_64.txt b/ydb/library/yql/core/CMakeLists.windows-x86_64.txt
index b46783d4f6..6b6010bdea 100644
--- a/ydb/library/yql/core/CMakeLists.windows-x86_64.txt
+++ b/ydb/library/yql/core/CMakeLists.windows-x86_64.txt
@@ -20,6 +20,7 @@ add_subdirectory(peephole_opt)
add_subdirectory(services)
add_subdirectory(sql_types)
add_subdirectory(type_ann)
+add_subdirectory(url_preprocessing)
add_subdirectory(user_data)
add_subdirectory(ut)
get_built_tool_path(
diff --git a/ydb/library/yql/core/url_preprocessing/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/core/url_preprocessing/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..e94218ed23
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-core-url_preprocessing)
+target_link_libraries(yql-core-url_preprocessing PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ yql-utils-log
+ cpp-regex-pcre
+)
+target_sources(yql-core-url_preprocessing PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_mapper.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/pattern_group.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp
+)
diff --git a/ydb/library/yql/core/url_preprocessing/CMakeLists.linux-aarch64.txt b/ydb/library/yql/core/url_preprocessing/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..8f61fc1505
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-core-url_preprocessing)
+target_link_libraries(yql-core-url_preprocessing PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ yql-utils-log
+ cpp-regex-pcre
+)
+target_sources(yql-core-url_preprocessing PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_mapper.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/pattern_group.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp
+)
diff --git a/ydb/library/yql/core/url_preprocessing/CMakeLists.linux-x86_64.txt b/ydb/library/yql/core/url_preprocessing/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..8f61fc1505
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-core-url_preprocessing)
+target_link_libraries(yql-core-url_preprocessing PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ yql-utils-log
+ cpp-regex-pcre
+)
+target_sources(yql-core-url_preprocessing PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_mapper.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/pattern_group.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp
+)
diff --git a/ydb/library/yql/core/url_preprocessing/CMakeLists.txt b/ydb/library/yql/core/url_preprocessing/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/core/url_preprocessing/CMakeLists.windows-x86_64.txt b/ydb/library/yql/core/url_preprocessing/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..e94218ed23
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-core-url_preprocessing)
+target_link_libraries(yql-core-url_preprocessing PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ yql-utils-log
+ cpp-regex-pcre
+)
+target_sources(yql-core-url_preprocessing PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_mapper.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/pattern_group.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp
+)
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..de914ed783
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/ya.make
@@ -0,0 +1,16 @@
+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()
+
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/CMakeLists.txt b/ydb/library/yql/providers/CMakeLists.txt
index e6ab81d4f9..e3d39dbe7e 100644
--- a/ydb/library/yql/providers/CMakeLists.txt
+++ b/ydb/library/yql/providers/CMakeLists.txt
@@ -16,4 +16,6 @@ add_subdirectory(pq)
add_subdirectory(result)
add_subdirectory(s3)
add_subdirectory(solomon)
+add_subdirectory(stat)
add_subdirectory(ydb)
+add_subdirectory(yt)
diff --git a/ydb/library/yql/providers/stat/CMakeLists.txt b/ydb/library/yql/providers/stat/CMakeLists.txt
new file mode 100644
index 0000000000..aa889ce713
--- /dev/null
+++ b/ydb/library/yql/providers/stat/CMakeLists.txt
@@ -0,0 +1,10 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(expr_nodes)
+add_subdirectory(uploader)
diff --git a/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..ecd82e499c
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,41 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-stat-expr_nodes)
+target_link_libraries(providers-stat-expr_nodes PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-stat-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
diff --git a/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..062f55f7cc
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-stat-expr_nodes)
+target_link_libraries(providers-stat-expr_nodes PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-stat-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
diff --git a/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..062f55f7cc
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-stat-expr_nodes)
+target_link_libraries(providers-stat-expr_nodes PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-stat-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
diff --git a/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.txt b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..ecd82e499c
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,41 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-stat-expr_nodes)
+target_link_libraries(providers-stat-expr_nodes PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-stat-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/stat/uploader/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..2e72020a05
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-stat-uploader)
+target_link_libraries(providers-stat-uploader PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-future
+)
+target_sources(providers-stat-uploader PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp
+)
diff --git a/ydb/library/yql/providers/stat/uploader/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/stat/uploader/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..d066df8667
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-stat-uploader)
+target_link_libraries(providers-stat-uploader PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-future
+)
+target_sources(providers-stat-uploader PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp
+)
diff --git a/ydb/library/yql/providers/stat/uploader/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/stat/uploader/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..d066df8667
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-stat-uploader)
+target_link_libraries(providers-stat-uploader PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-future
+)
+target_sources(providers-stat-uploader PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp
+)
diff --git a/ydb/library/yql/providers/stat/uploader/CMakeLists.txt b/ydb/library/yql/providers/stat/uploader/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/stat/uploader/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/stat/uploader/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..2e72020a05
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-stat-uploader)
+target_link_libraries(providers-stat-uploader PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-threading-future
+)
+target_sources(providers-stat-uploader PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp
+)
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/yt/CMakeLists.txt b/ydb/library/yql/providers/yt/CMakeLists.txt
new file mode 100644
index 0000000000..e38ff3d7da
--- /dev/null
+++ b/ydb/library/yql/providers/yt/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(codec)
+add_subdirectory(common)
+add_subdirectory(comp_nodes)
+add_subdirectory(expr_nodes)
+add_subdirectory(gateway)
+add_subdirectory(job)
+add_subdirectory(lib)
+add_subdirectory(opt)
+add_subdirectory(provider)
diff --git a/ydb/library/yql/providers/yt/codec/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/codec/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..a2627c4a3e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(codegen)
+
+add_library(providers-yt-codec)
+target_compile_options(providers-yt-codec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-codec PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-common
+ yt-lib-mkql_helpers
+ yt-lib-skiff
+ yt-codec-codegen
+)
+target_sources(providers-yt-codec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_job.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec.cpp
+)
diff --git a/ydb/library/yql/providers/yt/codec/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/codec/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..a0a504ebb8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,39 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(codegen)
+
+add_library(providers-yt-codec)
+target_compile_options(providers-yt-codec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-codec PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-common
+ yt-lib-mkql_helpers
+ yt-lib-skiff
+ yt-codec-codegen
+)
+target_sources(providers-yt-codec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_job.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec.cpp
+)
diff --git a/ydb/library/yql/providers/yt/codec/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/codec/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..a0a504ebb8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,39 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(codegen)
+
+add_library(providers-yt-codec)
+target_compile_options(providers-yt-codec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-codec PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-common
+ yt-lib-mkql_helpers
+ yt-lib-skiff
+ yt-codec-codegen
+)
+target_sources(providers-yt-codec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_job.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec.cpp
+)
diff --git a/ydb/library/yql/providers/yt/codec/CMakeLists.txt b/ydb/library/yql/providers/yt/codec/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/codec/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/codec/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..a2627c4a3e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(codegen)
+
+add_library(providers-yt-codec)
+target_compile_options(providers-yt-codec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-codec PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-common
+ yt-lib-mkql_helpers
+ yt-lib-skiff
+ yt-codec-codegen
+)
+target_sources(providers-yt-codec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec_job.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/yt_codec.cpp
+)
diff --git a/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..0105a602cf
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,105 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_rescompiler_bin
+ TOOL_rescompiler_dependency
+ tools/rescompiler/bin
+ rescompiler
+)
+
+add_library(yt-codec-codegen)
+target_compile_options(yt-codec-codegen PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp
+)
+
+add_global_library_for(yt-codec-codegen.global yt-codec-codegen)
+target_compile_options(yt-codec-codegen.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen.global PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen.global PRIVATE
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ COMMAND
+ ${LLVMOPT}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ -O2
+ -globalopt
+ -globaldce
+ -internalize
+ -internalize-public-api-list=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
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ COMMAND
+ ${LLVMLINK}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+)
+llvm_compile_cxx(yt-codec-codegen.global
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ ${CLANGPLUSPLUS}
+ -Wno-unknown-warning-option
+ -emit-llvm
+)
+resources(yt-codec-codegen.global
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+ INPUTS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ KEYS
+ /llvm_bc/YtCodecFuncs
+)
diff --git a/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..fba2ebc670
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,107 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_rescompiler_bin
+ TOOL_rescompiler_dependency
+ tools/rescompiler/bin
+ rescompiler
+)
+
+add_library(yt-codec-codegen)
+target_compile_options(yt-codec-codegen PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp
+)
+
+add_global_library_for(yt-codec-codegen.global yt-codec-codegen)
+target_compile_options(yt-codec-codegen.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen.global PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen.global PRIVATE
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ COMMAND
+ ${LLVMOPT}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ -O2
+ -globalopt
+ -globaldce
+ -internalize
+ -internalize-public-api-list=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
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ COMMAND
+ ${LLVMLINK}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+)
+llvm_compile_cxx(yt-codec-codegen.global
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ ${CLANGPLUSPLUS}
+ -Wno-unknown-warning-option
+ -emit-llvm
+)
+resources(yt-codec-codegen.global
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+ INPUTS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ KEYS
+ /llvm_bc/YtCodecFuncs
+)
diff --git a/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..fba2ebc670
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,107 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_rescompiler_bin
+ TOOL_rescompiler_dependency
+ tools/rescompiler/bin
+ rescompiler
+)
+
+add_library(yt-codec-codegen)
+target_compile_options(yt-codec-codegen PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp
+)
+
+add_global_library_for(yt-codec-codegen.global yt-codec-codegen)
+target_compile_options(yt-codec-codegen.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen.global PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen.global PRIVATE
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ COMMAND
+ ${LLVMOPT}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ -O2
+ -globalopt
+ -globaldce
+ -internalize
+ -internalize-public-api-list=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
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ COMMAND
+ ${LLVMLINK}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+)
+llvm_compile_cxx(yt-codec-codegen.global
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ ${CLANGPLUSPLUS}
+ -Wno-unknown-warning-option
+ -emit-llvm
+)
+resources(yt-codec-codegen.global
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+ INPUTS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ KEYS
+ /llvm_bc/YtCodecFuncs
+)
diff --git a/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.txt b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..0105a602cf
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,105 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_rescompiler_bin
+ TOOL_rescompiler_dependency
+ tools/rescompiler/bin
+ rescompiler
+)
+
+add_library(yt-codec-codegen)
+target_compile_options(yt-codec-codegen PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp
+)
+
+add_global_library_for(yt-codec-codegen.global yt-codec-codegen)
+target_compile_options(yt-codec-codegen.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-codec-codegen.global PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-resource
+ ydb-library-binary_json
+ minikql-computation-llvm
+ parser-pg_wrapper-interface
+ library-yql-utils
+ llvm12-lib-IR
+ lib-ExecutionEngine-MCJIT
+ llvm12-lib-Linker
+ llvm12-lib-Support
+ lib-Target-X86
+ Target-X86-AsmParser
+ lib-Transforms-IPO
+ yql-minikql-codegen
+)
+target_sources(yt-codec-codegen.global PRIVATE
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ COMMAND
+ ${LLVMOPT}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ -O2
+ -globalopt
+ -globaldce
+ -internalize
+ -internalize-public-api-list=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
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+ DEPENDS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ COMMAND
+ ${LLVMLINK}
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ -o
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_merged.bc
+)
+llvm_compile_cxx(yt-codec-codegen.global
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp.bc
+ ${CLANGPLUSPLUS}
+ -Wno-unknown-warning-option
+ -emit-llvm
+)
+resources(yt-codec-codegen.global
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/12ec3101983db48f6ee4095b4f5fb784.cpp
+ INPUTS
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/codec/codegen/YtCodecFuncs_optimized.bc
+ KEYS
+ /llvm_bc/YtCodecFuncs
+)
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..9268ecf4a0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp
@@ -0,0 +1,2277 @@
+#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, TMaybe<ui64> currentRow, TMaybe<ui32> currentRange) {
+ Reader_ = MakeBlockReader(source, blockCount, blockSize);
+ Buf_.SetSource(*Reader_);
+ HasRangeIndices_ = source.HasRangeIndices();
+ InitialTableIndex_ = tableIndex;
+ IgnoreStreamTableIndex_ = ignoreStreamTableIndex;
+ if (Decoder_) {
+ Decoder_->Reset(HasRangeIndices_, InitialTableIndex_, IgnoreStreamTableIndex_);
+ if (currentRow) {
+ Decoder_->RowIndex_ = currentRow;
+ }
+ if (currentRange) {
+ Decoder_->RangeIndex_ = currentRange;
+ }
+ }
+}
+
+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_;
+}
+
+TMaybe<ui64> TMkqlReaderImpl::GetRowIndexUnchecked() const {
+ return Decoder_ ? Decoder_->RowIndex_ : TMaybe<ui64>();
+}
+
+TMaybe<ui32> TMkqlReaderImpl::GetRangeIndexUnchecked() const {
+ return Decoder_ ? Decoder_->RangeIndex_ : TMaybe<ui32>();
+}
+
+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..bdc5af326b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec_io.h
@@ -0,0 +1,163 @@
+#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, TMaybe<ui64> currentRow={}, TMaybe<ui32> currentRange={});
+ 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;
+ TMaybe<ui32> GetRangeIndexUnchecked() const;
+ TMaybe<ui64> GetRowIndexUnchecked() const;
+ 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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/common/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..3609b9840e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-common)
+target_compile_options(providers-yt-common PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-common PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-parse_size
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-utils-log
+ providers-common-codec
+ providers-common-config
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_configuration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_names.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp
+)
+generate_enum_serilization(providers-yt-common
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/common/yql_yt_settings.h
+)
diff --git a/ydb/library/yql/providers/yt/common/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/common/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..7ff0e18318
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,43 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-common)
+target_compile_options(providers-yt-common PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-common PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-parse_size
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-utils-log
+ providers-common-codec
+ providers-common-config
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_configuration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_names.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp
+)
+generate_enum_serilization(providers-yt-common
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/common/yql_yt_settings.h
+)
diff --git a/ydb/library/yql/providers/yt/common/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/common/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..7ff0e18318
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,43 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-common)
+target_compile_options(providers-yt-common PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-common PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-parse_size
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-utils-log
+ providers-common-codec
+ providers-common-config
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_configuration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_names.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp
+)
+generate_enum_serilization(providers-yt-common
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/common/yql_yt_settings.h
+)
diff --git a/ydb/library/yql/providers/yt/common/CMakeLists.txt b/ydb/library/yql/providers/yt/common/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/common/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/common/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..3609b9840e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-common)
+target_compile_options(providers-yt-common PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-common PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-parse_size
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-utils-log
+ providers-common-codec
+ providers-common-config
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_configuration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_names.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp
+)
+generate_enum_serilization(providers-yt-common
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/common/yql_yt_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/common/yql_yt_settings.h
+)
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..ca01b761eb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_configuration.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <util/system/types.h>
+#include <util/datetime/base.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 TDuration DEFAULT_RPC_READER_TIMEOUT = TDuration::Seconds(120);
+
+
+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..27bd85cb41
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp
@@ -0,0 +1,511 @@
+#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);
+ 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(2).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(2).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, DQRPCReaderTimeout);
+ 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;
+ });
+ REGISTER_SETTING(*this, CostBasedOptimizer).Parser([](const TString& v) { return FromString<ECostBasedOptimizer>(v); });;
+}
+
+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..2b0fd6f6f3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_yt_settings.h
@@ -0,0 +1,343 @@
+#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" */,
+};
+
+enum class ECostBasedOptimizer {
+ Disable,
+ PG,
+};
+
+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;
+ NCommon::TConfSetting<TDuration, true> DQRPCReaderTimeout;
+
+ // 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;
+ NCommon::TConfSetting<ECostBasedOptimizer, false> CostBasedOptimizer;
+};
+
+EReleaseTempDataMode GetReleaseTempDataMode(const TYtSettings& settings);
+EJoinCollectColumnarStatisticsMode GetJoinCollectColumnarStatisticsMode(const TYtSettings& settings);
+inline TString GetTablesTmpFolder(const TYtSettings& settings) {
+ return settings.TablesTmpFolder.Get().GetOrElse(settings.TmpFolder.Get().GetOrElse({}));
+}
+
+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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..c4bc336489
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,34 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-comp_nodes)
+target_compile_options(providers-yt-comp_nodes PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-comp_nodes PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-common-mkql
+ providers-yt-codec
+ providers-yt-expr_nodes
+)
+target_sources(providers-yt-comp_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp
+)
diff --git a/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..a60a2a41b1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,35 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-comp_nodes)
+target_compile_options(providers-yt-comp_nodes PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-comp_nodes PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-common-mkql
+ providers-yt-codec
+ providers-yt-expr_nodes
+)
+target_sources(providers-yt-comp_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp
+)
diff --git a/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..a60a2a41b1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,35 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-comp_nodes)
+target_compile_options(providers-yt-comp_nodes PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-comp_nodes PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-common-mkql
+ providers-yt-codec
+ providers-yt-expr_nodes
+)
+target_sources(providers-yt-comp_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp
+)
diff --git a/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.txt b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..c4bc336489
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,34 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-comp_nodes)
+target_compile_options(providers-yt-comp_nodes PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-comp_nodes PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-streams-brotli
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-common-mkql
+ providers-yt-codec
+ providers-yt-expr_nodes
+)
+target_sources(providers-yt-comp_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..ed0dc39cb7
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,41 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-yt-expr_nodes)
+target_link_libraries(providers-yt-expr_nodes PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-yt-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
diff --git a/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..b1b99d65f9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-yt-expr_nodes)
+target_link_libraries(providers-yt-expr_nodes PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-yt-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
diff --git a/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..b1b99d65f9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-yt-expr_nodes)
+target_link_libraries(providers-yt-expr_nodes PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-yt-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
diff --git a/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.txt b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..ed0dc39cb7
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,41 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(Python3 REQUIRED)
+
+add_library(providers-yt-expr_nodes)
+target_link_libraries(providers-yt-expr_nodes PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(providers-yt-expr_nodes PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
+add_custom_command(
+ OUTPUT
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+ DEPENDS
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ COMMAND
+ Python3::Interpreter
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.jnj
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h
+ ${CMAKE_BINARY_DIR}/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h
+)
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..9424756cfc
--- /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_PYTHON3(
+ ${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/CMakeLists.txt b/ydb/library/yql/providers/yt/gateway/CMakeLists.txt
new file mode 100644
index 0000000000..e4353f2ae0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/CMakeLists.txt
@@ -0,0 +1,10 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(lib)
+add_subdirectory(native)
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..e5b2f266ce
--- /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 = GetTablesTmpFolder(*options.Config());
+ 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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..6721e4c5ba
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,44 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-lib)
+target_compile_options(yt-gateway-lib PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-lib PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-url
+ cpp-threading-future
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ yql-core-file_storage
+ yql-public-issue
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ yql-core-type_ann
+ providers-common-codec
+ providers-common-gateway
+ providers-yt-common
+ yt-lib-hash
+ yt-lib-res_pull
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+)
+target_sources(yt-gateway-lib PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/user_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..fee6ac163b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,45 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-lib)
+target_compile_options(yt-gateway-lib PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-lib PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-url
+ cpp-threading-future
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ yql-core-file_storage
+ yql-public-issue
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ yql-core-type_ann
+ providers-common-codec
+ providers-common-gateway
+ providers-yt-common
+ yt-lib-hash
+ yt-lib-res_pull
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+)
+target_sources(yt-gateway-lib PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/user_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..fee6ac163b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,45 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-lib)
+target_compile_options(yt-gateway-lib PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-lib PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-url
+ cpp-threading-future
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ yql-core-file_storage
+ yql-public-issue
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ yql-core-type_ann
+ providers-common-codec
+ providers-common-gateway
+ providers-yt-common
+ yt-lib-hash
+ yt-lib-res_pull
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+)
+target_sources(yt-gateway-lib PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/user_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.txt b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..6721e4c5ba
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,44 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-lib)
+target_compile_options(yt-gateway-lib PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-lib PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-regex-pcre
+ cpp-string_utils-url
+ cpp-threading-future
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ yql-core-file_storage
+ yql-public-issue
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ yql-core-type_ann
+ providers-common-codec
+ providers-common-gateway
+ providers-yt-common
+ yt-lib-hash
+ yt-lib-res_pull
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+)
+target_sources(yt-gateway-lib PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/user_files.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp
+)
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..8d45103697
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp
@@ -0,0 +1,380 @@
+#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) {
+ // Yt returns NoSuchTransaction as inner issue for ResolveError
+ if (!e.IsResolveError() || e.IsNoSuchTransaction()) {
+ 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..9ad5e08813
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp
@@ -0,0 +1,465 @@
+#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) {
+ // Yt returns NoSuchTransaction as inner issue for ResolveError
+ if (!e.IsResolveError() || e.IsNoSuchTransaction()) {
+ 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 = GetTablesTmpFolder(*config);
+
+ 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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..b0b5b9bfdb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,73 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-native)
+target_compile_options(yt-gateway-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-native PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-containers-sorted_vector
+ cpp-digest-md5
+ library-cpp-random_provider
+ cpp-streams-brotli
+ cpp-threading-future
+ library-cpp-time_provider
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-file_storage
+ minikql-comp_nodes-llvm
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ providers-common-proto
+ providers-common-provider
+ common-schema-expr
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-stat-uploader
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-gateway-lib
+ providers-yt-job
+ yt-lib-expr_traits
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-log
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-schema
+ yt-lib-skiff
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+ yt-lib-init_yt_api
+ yt-lib-config_clusters
+ providers-yt-provider
+)
+target_sources(yt-gateway-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp
+)
diff --git a/ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..80b7c65cdc
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,74 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-native)
+target_compile_options(yt-gateway-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-native PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-containers-sorted_vector
+ cpp-digest-md5
+ library-cpp-random_provider
+ cpp-streams-brotli
+ cpp-threading-future
+ library-cpp-time_provider
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-file_storage
+ minikql-comp_nodes-llvm
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ providers-common-proto
+ providers-common-provider
+ common-schema-expr
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-stat-uploader
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-gateway-lib
+ providers-yt-job
+ yt-lib-expr_traits
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-log
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-schema
+ yt-lib-skiff
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+ yt-lib-init_yt_api
+ yt-lib-config_clusters
+ providers-yt-provider
+)
+target_sources(yt-gateway-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp
+)
diff --git a/ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..80b7c65cdc
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,74 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-native)
+target_compile_options(yt-gateway-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-native PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-containers-sorted_vector
+ cpp-digest-md5
+ library-cpp-random_provider
+ cpp-streams-brotli
+ cpp-threading-future
+ library-cpp-time_provider
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-file_storage
+ minikql-comp_nodes-llvm
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ providers-common-proto
+ providers-common-provider
+ common-schema-expr
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-stat-uploader
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-gateway-lib
+ providers-yt-job
+ yt-lib-expr_traits
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-log
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-schema
+ yt-lib-skiff
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+ yt-lib-init_yt_api
+ yt-lib-config_clusters
+ providers-yt-provider
+)
+target_sources(yt-gateway-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp
+)
diff --git a/ydb/library/yql/providers/yt/gateway/native/CMakeLists.txt b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/gateway/native/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..b0b5b9bfdb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,73 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-gateway-native)
+target_compile_options(yt-gateway-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-gateway-native PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-containers-sorted_vector
+ cpp-digest-md5
+ library-cpp-random_provider
+ cpp-streams-brotli
+ cpp-threading-future
+ library-cpp-time_provider
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-file_storage
+ minikql-comp_nodes-llvm
+ library-yql-utils
+ yql-utils-log
+ yql-utils-threading
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ providers-common-proto
+ providers-common-provider
+ common-schema-expr
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-stat-uploader
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-gateway-lib
+ providers-yt-job
+ yt-lib-expr_traits
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-log
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-schema
+ yt-lib-skiff
+ yt-lib-url_mapper
+ yt-lib-yson_helpers
+ yt-lib-init_yt_api
+ yt-lib-config_clusters
+ providers-yt-provider
+)
+target_sources(yt-gateway-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp
+)
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..6c4c17441e
--- /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 = GetTablesTmpFolder(*settings);
+
+ 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 = GetTablesTmpFolder(*settings);
+ 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 = GetTablesTmpFolder(*settings);
+ 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..1c9b3b3355
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp
@@ -0,0 +1,4908 @@
+#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 = GetTablesTmpFolder(*options.Config());
+ 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 = GetTablesTmpFolder(*options.Config());
+
+ 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 = GetTablesTmpFolder(*options.Config());
+ 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 = GetTablesTmpFolder(*options.Config());
+ 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(GetTablesTmpFolder(*options.Config()), 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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 = GetTablesTmpFolder(*options.Config());
+
+ 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) {
+ // Yt returns NoSuchTransaction as inner issue for ResolveError
+ if (!e.IsResolveError() || e.IsNoSuchTransaction()) {
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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 = GetTablesTmpFolder(*options.Config());
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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 = GetTablesTmpFolder(*execCtx->Options_.Config());
+ 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..6f7cbd90fc
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp
@@ -0,0 +1,582 @@
+#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)) {
+ spec["enable_job_splitting"] = false;
+ dataSizePerJob = GetCombiningDataSizePerJob(dataSizePerJob, settings->MinPublishedAvgChunkSize.Get());
+ } else if (opProps.HasFlags(EYtOpProp::TemporaryChunkCombine)) {
+ spec["enable_job_splitting"] = false;
+ 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 = GetTablesTmpFolder(*settings)) {
+ 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..e363e9e751
--- /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 = GetTablesTmpFolder(*Settings_);
+ 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/job/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/job/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..8de7f28083
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,47 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-job)
+target_compile_options(providers-yt-job PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-job PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ cpp-streams-brotli
+ library-cpp-time_provider
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-yt-user_job_statistics
+ minikql-comp_nodes-llvm
+ yql-public-udf
+ library-yql-utils
+ yql-utils-backtrace
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-comp_nodes
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-mkql_helpers
+)
+target_sources(providers-yt-job PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_base.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_calc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_factory.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_table_content.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_user.cpp
+)
diff --git a/ydb/library/yql/providers/yt/job/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/job/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..1aef76e6ae
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,48 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-job)
+target_compile_options(providers-yt-job PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-job PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ cpp-streams-brotli
+ library-cpp-time_provider
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-yt-user_job_statistics
+ minikql-comp_nodes-llvm
+ yql-public-udf
+ library-yql-utils
+ yql-utils-backtrace
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-comp_nodes
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-mkql_helpers
+)
+target_sources(providers-yt-job PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_base.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_calc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_factory.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_table_content.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_user.cpp
+)
diff --git a/ydb/library/yql/providers/yt/job/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/job/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..1aef76e6ae
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,48 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-job)
+target_compile_options(providers-yt-job PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-job PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ cpp-streams-brotli
+ library-cpp-time_provider
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-yt-user_job_statistics
+ minikql-comp_nodes-llvm
+ yql-public-udf
+ library-yql-utils
+ yql-utils-backtrace
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-comp_nodes
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-mkql_helpers
+)
+target_sources(providers-yt-job PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_base.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_calc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_factory.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_table_content.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_user.cpp
+)
diff --git a/ydb/library/yql/providers/yt/job/CMakeLists.txt b/ydb/library/yql/providers/yt/job/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/job/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/job/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..8de7f28083
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,47 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-job)
+target_compile_options(providers-yt-job PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-job PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ cpp-streams-brotli
+ library-cpp-time_provider
+ cpp-yson-node
+ cpp-mapreduce-interface
+ cpp-yt-user_job_statistics
+ minikql-comp_nodes-llvm
+ yql-public-udf
+ library-yql-utils
+ yql-utils-backtrace
+ providers-common-codec
+ providers-common-comp_nodes
+ providers-common-mkql
+ common-schema-mkql
+ common-schema-parser
+ providers-yt-codec
+ providers-yt-common
+ providers-yt-comp_nodes
+ yt-lib-infer_schema
+ yt-lib-lambda_builder
+ yt-lib-mkql_helpers
+)
+target_sources(providers-yt-job PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_base.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_calc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_factory.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_table_content.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/job/yql_job_user.cpp
+)
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/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/CMakeLists.txt
new file mode 100644
index 0000000000..862a7b1c34
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/CMakeLists.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(config_clusters)
+add_subdirectory(expr_traits)
+add_subdirectory(graph_reorder)
+add_subdirectory(hash)
+add_subdirectory(infer_schema)
+add_subdirectory(init_yt_api)
+add_subdirectory(key_filter)
+add_subdirectory(lambda_builder)
+add_subdirectory(log)
+add_subdirectory(mkql_helpers)
+add_subdirectory(res_pull)
+add_subdirectory(row_spec)
+add_subdirectory(schema)
+add_subdirectory(skiff)
+add_subdirectory(url_mapper)
+add_subdirectory(yson_helpers)
+add_subdirectory(yt_download)
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..65bef0287b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-config_clusters)
+target_link_libraries(yt-lib-config_clusters PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+)
+target_sources(yt-lib-config_clusters PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..5ea0218edb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-config_clusters)
+target_link_libraries(yt-lib-config_clusters PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+)
+target_sources(yt-lib-config_clusters PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..5ea0218edb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-config_clusters)
+target_link_libraries(yt-lib-config_clusters PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+)
+target_sources(yt-lib-config_clusters PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..65bef0287b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-config_clusters)
+target_link_libraries(yt-lib-config_clusters PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+)
+target_sources(yt-lib-config_clusters PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..043abfbe19
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-expr_traits)
+target_compile_options(yt-lib-expr_traits PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-expr_traits PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ minikql-computation-llvm
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-expr_traits PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..2f1f86b50e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,29 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-expr_traits)
+target_compile_options(yt-lib-expr_traits PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-expr_traits PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ minikql-computation-llvm
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-expr_traits PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..2f1f86b50e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,29 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-expr_traits)
+target_compile_options(yt-lib-expr_traits PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-expr_traits PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ minikql-computation-llvm
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-expr_traits PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..043abfbe19
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-expr_traits)
+target_compile_options(yt-lib-expr_traits PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-expr_traits PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ minikql-computation-llvm
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-expr_traits PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..cbf80b673c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-graph_reorder)
+target_compile_options(yt-lib-graph_reorder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-graph_reorder PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(yt-lib-graph_reorder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..5e25d9537f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-graph_reorder)
+target_compile_options(yt-lib-graph_reorder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-graph_reorder PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(yt-lib-graph_reorder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..5e25d9537f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-graph_reorder)
+target_compile_options(yt-lib-graph_reorder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-graph_reorder PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(yt-lib-graph_reorder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..cbf80b673c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-graph_reorder)
+target_compile_options(yt-lib-graph_reorder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-graph_reorder PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-ast
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ providers-common-provider
+)
+target_sources(yt-lib-graph_reorder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..2a6cdde0af
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-lib-hash)
+target_link_libraries(yt-lib-hash PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ OpenSSL::OpenSSL
+ library-yql-ast
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+)
+target_sources(yt-lib-hash PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..797f989484
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-lib-hash)
+target_link_libraries(yt-lib-hash PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ OpenSSL::OpenSSL
+ library-yql-ast
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+)
+target_sources(yt-lib-hash PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..797f989484
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-lib-hash)
+target_link_libraries(yt-lib-hash PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ OpenSSL::OpenSSL
+ library-yql-ast
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+)
+target_sources(yt-lib-hash PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/hash/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/hash/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..2a6cdde0af
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-lib-hash)
+target_link_libraries(yt-lib-hash PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ OpenSSL::OpenSSL
+ library-yql-ast
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+)
+target_sources(yt-lib-hash PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp
+)
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..4194364770
--- /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 <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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..32bf9cc3ad
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-infer_schema)
+target_link_libraries(yt-lib-infer_schema PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ yql-public-issue
+ yql-utils-log
+ yql-core-issue
+)
+target_sources(yt-lib-infer_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..83d6e71a1b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-infer_schema)
+target_link_libraries(yt-lib-infer_schema PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ yql-public-issue
+ yql-utils-log
+ yql-core-issue
+)
+target_sources(yt-lib-infer_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..83d6e71a1b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-infer_schema)
+target_link_libraries(yt-lib-infer_schema PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ yql-public-issue
+ yql-utils-log
+ yql-core-issue
+)
+target_sources(yt-lib-infer_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..32bf9cc3ad
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-infer_schema)
+target_link_libraries(yt-lib-infer_schema PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ yql-public-issue
+ yql-utils-log
+ yql-core-issue
+)
+target_sources(yt-lib-infer_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..37772b3ac5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-init_yt_api)
+target_link_libraries(yt-lib-init_yt_api PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-utils-log
+ cpp-mapreduce-common
+ cpp-mapreduce-client
+ cpp-yson-node
+)
+target_sources(yt-lib-init_yt_api PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..ec008b65eb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-init_yt_api)
+target_link_libraries(yt-lib-init_yt_api PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-utils-log
+ cpp-mapreduce-common
+ cpp-mapreduce-client
+ cpp-yson-node
+)
+target_sources(yt-lib-init_yt_api PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..ec008b65eb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-init_yt_api)
+target_link_libraries(yt-lib-init_yt_api PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-utils-log
+ cpp-mapreduce-common
+ cpp-mapreduce-client
+ cpp-yson-node
+)
+target_sources(yt-lib-init_yt_api PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..37772b3ac5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-init_yt_api)
+target_link_libraries(yt-lib-init_yt_api PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-utils-log
+ cpp-mapreduce-common
+ cpp-mapreduce-client
+ cpp-yson-node
+)
+target_sources(yt-lib-init_yt_api PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..9a86d0f745
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-key_filter)
+target_compile_options(yt-lib-key_filter PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-key_filter PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-utils
+ library-yql-ast
+)
+target_sources(yt-lib-key_filter PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..66e6cd0979
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-key_filter)
+target_compile_options(yt-lib-key_filter PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-key_filter PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-utils
+ library-yql-ast
+)
+target_sources(yt-lib-key_filter PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..66e6cd0979
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-key_filter)
+target_compile_options(yt-lib-key_filter PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-key_filter PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-utils
+ library-yql-ast
+)
+target_sources(yt-lib-key_filter PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..9a86d0f745
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-key_filter)
+target_compile_options(yt-lib-key_filter PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-key_filter PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-utils
+ library-yql-ast
+)
+target_sources(yt-lib-key_filter PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..aaf3957c88
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-lambda_builder)
+target_compile_options(yt-lib-lambda_builder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-lambda_builder PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ library-cpp-time_provider
+ library-yql-ast
+ minikql-computation-llvm
+ yql-public-udf
+ library-yql-utils
+ providers-common-mkql
+)
+target_sources(yt-lib-lambda_builder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..d4bdc5f092
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-lambda_builder)
+target_compile_options(yt-lib-lambda_builder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-lambda_builder PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ library-cpp-time_provider
+ library-yql-ast
+ minikql-computation-llvm
+ yql-public-udf
+ library-yql-utils
+ providers-common-mkql
+)
+target_sources(yt-lib-lambda_builder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..d4bdc5f092
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-lambda_builder)
+target_compile_options(yt-lib-lambda_builder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-lambda_builder PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ library-cpp-time_provider
+ library-yql-ast
+ minikql-computation-llvm
+ yql-public-udf
+ library-yql-utils
+ providers-common-mkql
+)
+target_sources(yt-lib-lambda_builder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..aaf3957c88
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-lambda_builder)
+target_compile_options(yt-lib-lambda_builder PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-lambda_builder PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-random_provider
+ library-cpp-time_provider
+ library-yql-ast
+ minikql-computation-llvm
+ yql-public-udf
+ library-yql-utils
+ providers-common-mkql
+)
+target_sources(yt-lib-lambda_builder PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/log/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..a9eff09932
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,20 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-log)
+target_link_libraries(yt-lib-log PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ mapreduce-interface-logging
+ yql-utils-log
+)
+target_sources(yt-lib-log PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/log/yt_logger.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..1e2645d272
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-log)
+target_link_libraries(yt-lib-log PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ mapreduce-interface-logging
+ yql-utils-log
+)
+target_sources(yt-lib-log PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/log/yt_logger.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..1e2645d272
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-log)
+target_link_libraries(yt-lib-log PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ mapreduce-interface-logging
+ yql-utils-log
+)
+target_sources(yt-lib-log PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/log/yt_logger.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/log/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/log/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/log/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/log/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..a9eff09932
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,20 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-log)
+target_link_libraries(yt-lib-log PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ mapreduce-interface-logging
+ yql-utils-log
+)
+target_sources(yt-lib-log PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/log/yt_logger.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..a01227981b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-mkql_helpers)
+target_compile_options(yt-lib-mkql_helpers PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-mkql_helpers PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-minikql
+ library-yql-core
+ library-yql-ast
+ library-yql-utils
+)
+target_sources(yt-lib-mkql_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..28edbf50ab
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-mkql_helpers)
+target_compile_options(yt-lib-mkql_helpers PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-mkql_helpers PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-minikql
+ library-yql-core
+ library-yql-ast
+ library-yql-utils
+)
+target_sources(yt-lib-mkql_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..28edbf50ab
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-mkql_helpers)
+target_compile_options(yt-lib-mkql_helpers PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-mkql_helpers PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-minikql
+ library-yql-core
+ library-yql-ast
+ library-yql-utils
+)
+target_sources(yt-lib-mkql_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..a01227981b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-mkql_helpers)
+target_compile_options(yt-lib-mkql_helpers PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-mkql_helpers PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-yql-minikql
+ library-yql-core
+ library-yql-ast
+ library-yql-utils
+)
+target_sources(yt-lib-mkql_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..e632b9235b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-res_pull)
+target_compile_options(yt-lib-res_pull PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-res_pull PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-yt-codec
+ yt-lib-mkql_helpers
+)
+target_sources(yt-lib-res_pull PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..efb5f881b6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,29 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-res_pull)
+target_compile_options(yt-lib-res_pull PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-res_pull PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-yt-codec
+ yt-lib-mkql_helpers
+)
+target_sources(yt-lib-res_pull PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..efb5f881b6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,29 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-res_pull)
+target_compile_options(yt-lib-res_pull PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-res_pull PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-yt-codec
+ yt-lib-mkql_helpers
+)
+target_sources(yt-lib-res_pull PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..e632b9235b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-res_pull)
+target_compile_options(yt-lib-res_pull PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-res_pull PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ library-yql-minikql
+ yql-public-udf
+ library-yql-utils
+ providers-common-codec
+ providers-yt-codec
+ yt-lib-mkql_helpers
+)
+target_sources(yt-lib-res_pull PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..54481cb9cc
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-row_spec)
+target_compile_options(yt-lib-row_spec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-row_spec PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-yql-ast
+ yql-core-expr_nodes_gen
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-provider
+ providers-common-schema
+ common-schema-expr
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-row_spec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..9e05db4766
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,33 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-row_spec)
+target_compile_options(yt-lib-row_spec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-row_spec PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-yql-ast
+ yql-core-expr_nodes_gen
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-provider
+ providers-common-schema
+ common-schema-expr
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-row_spec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..9e05db4766
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,33 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-row_spec)
+target_compile_options(yt-lib-row_spec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-row_spec PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-yql-ast
+ yql-core-expr_nodes_gen
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-provider
+ providers-common-schema
+ common-schema-expr
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-row_spec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..54481cb9cc
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-row_spec)
+target_compile_options(yt-lib-row_spec PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yt-lib-row_spec PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-yql-ast
+ yql-core-expr_nodes_gen
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ providers-common-codec
+ providers-common-provider
+ providers-common-schema
+ common-schema-expr
+ providers-yt-common
+ providers-yt-expr_nodes
+)
+target_sources(yt-lib-row_spec PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp
+)
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..b4bcf59ff2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp
@@ -0,0 +1,1643 @@
+#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;
+ };
+
+ const auto setToNode = [pathToNode](const TConstraintNode::TSetType& set) -> NYT::TNode {
+ if (1U == set.size() && 1U == set.front().size())
+ return TStringBuf(set.front().front());
+
+ auto list = NYT::TNode::CreateList();
+ for (const auto& path : set)
+ list.Add(pathToNode(path));
+ 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& sets : Unique->GetContent()) {
+ auto part = NYT::TNode::CreateList();
+ for (const auto& set : sets)
+ part.Add(setToNode(set));
+ list.Add(part);
+ }
+ map[Unique->GetName()] = list;
+ }
+
+ if (Distinct) {
+ auto list = NYT::TNode::CreateList();
+ for (const auto& sets : Distinct->GetContent()) {
+ auto part = NYT::TNode::CreateList();
+ for (const auto& set : sets)
+ part.Add(setToNode(set));
+ list.Add(part);
+ }
+ 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;
+
+ try {
+ const auto nodeToPath = [&ctx](const NYT::TNode& node) {
+ if (node.IsString())
+ return TConstraintNode::TPathType{ctx.AppendString(node.AsString())};
+
+ TConstraintNode::TPathType path;
+ for (const auto& col : node.AsList())
+ path.emplace_back(ctx.AppendString(col.AsString()));
+ return path;
+ };
+
+ const auto nodeToSet = [&ctx, nodeToPath](const NYT::TNode& node) {
+ if (node.IsString())
+ return TConstraintNode::TSetType{TConstraintNode::TPathType(1U, ctx.AppendString(node.AsString()))};
+
+ TConstraintNode::TSetType set;
+ for (const auto& col : node.AsList())
+ set.insert_unique(nodeToPath(col));
+ return set;
+ };
+
+ 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::TContentType content;
+ for (const auto& item : it->second.AsList()) {
+ TConstraintNode::TSetOfSetsType sets;
+ for (const auto& part : item.AsList())
+ sets.insert_unique(nodeToSet(part));
+ content.insert_unique(std::move(sets));
+ }
+ if (!content.empty())
+ Unique = ctx.MakeConstraint<TUniqueConstraintNode>(std::move(content));
+ }
+ if (const auto it = constraints.find(TDistinctConstraintNode::Name()); constraints.cend() != it) {
+ TDistinctConstraintNode::TContentType content;
+ for (const auto& item : it->second.AsList()) {
+ TConstraintNode::TSetOfSetsType sets;
+ for (const auto& part : item.AsList())
+ sets.insert_unique(nodeToSet(part));
+ content.insert_unique(std::move(sets));
+ }
+ if (!content.empty())
+ Distinct = ctx.MakeConstraint<TDistinctConstraintNode>(std::move(content));
+ }
+ } catch (const yexception& error) {
+ Sorted = nullptr;
+ Unique = nullptr;
+ Distinct = nullptr;
+ YQL_CLOG(WARN, ProviderDq) << " Error '" << error << "' on parse constraints node: " << ConstraintsNode.AsString();
+ }
+}
+
+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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..0a26927e4a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-schema)
+target_link_libraries(yt-lib-schema PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-utils
+ yql-utils-log
+ providers-common-codec
+ providers-yt-common
+)
+target_sources(yt-lib-schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/schema/schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..13ad80d741
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-schema)
+target_link_libraries(yt-lib-schema PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-utils
+ yql-utils-log
+ providers-common-codec
+ providers-yt-common
+)
+target_sources(yt-lib-schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/schema/schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..13ad80d741
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-schema)
+target_link_libraries(yt-lib-schema PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-utils
+ yql-utils-log
+ providers-common-codec
+ providers-yt-common
+)
+target_sources(yt-lib-schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/schema/schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/schema/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/schema/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..0a26927e4a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-schema)
+target_link_libraries(yt-lib-schema PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ cpp-mapreduce-interface
+ library-yql-utils
+ yql-utils-log
+ providers-common-codec
+ providers-yt-common
+)
+target_sources(yt-lib-schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/schema/schema.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..262c3a5374
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-skiff)
+target_link_libraries(yt-lib-skiff PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ providers-yt-common
+ providers-common-codec
+ common-schema-skiff
+ library-yql-utils
+)
+target_sources(yt-lib-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..baa7fc1fbb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-skiff)
+target_link_libraries(yt-lib-skiff PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ providers-yt-common
+ providers-common-codec
+ common-schema-skiff
+ library-yql-utils
+)
+target_sources(yt-lib-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..baa7fc1fbb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-skiff)
+target_link_libraries(yt-lib-skiff PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ providers-yt-common
+ providers-common-codec
+ common-schema-skiff
+ library-yql-utils
+)
+target_sources(yt-lib-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..262c3a5374
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-skiff)
+target_link_libraries(yt-lib-skiff PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ providers-yt-common
+ providers-common-codec
+ common-schema-skiff
+ library-yql-utils
+)
+target_sources(yt-lib-skiff PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..46156a0caf
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-url_mapper)
+target_link_libraries(yt-lib-url_mapper PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ cpp-regex-pcre
+ library-cpp-uri
+ library-cpp-cgiparam
+)
+target_sources(yt-lib-url_mapper PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..9f2afeb3aa
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-url_mapper)
+target_link_libraries(yt-lib-url_mapper PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ cpp-regex-pcre
+ library-cpp-uri
+ library-cpp-cgiparam
+)
+target_sources(yt-lib-url_mapper PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..9f2afeb3aa
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-url_mapper)
+target_link_libraries(yt-lib-url_mapper PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ cpp-regex-pcre
+ library-cpp-uri
+ library-cpp-cgiparam
+)
+target_sources(yt-lib-url_mapper PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..46156a0caf
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-url_mapper)
+target_link_libraries(yt-lib-url_mapper PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ providers-common-proto
+ cpp-regex-pcre
+ library-cpp-uri
+ library-cpp-cgiparam
+)
+target_sources(yt-lib-url_mapper PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp
+)
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/yson_helpers/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..e67eddbb50
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yson_helpers)
+target_link_libraries(yt-lib-yson_helpers PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ cpp-yson-node
+ library-yql-utils
+ yql-utils-log
+ providers-yt-common
+)
+target_sources(yt-lib-yson_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..be84171baf
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yson_helpers)
+target_link_libraries(yt-lib-yson_helpers PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ cpp-yson-node
+ library-yql-utils
+ yql-utils-log
+ providers-yt-common
+)
+target_sources(yt-lib-yson_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..be84171baf
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yson_helpers)
+target_link_libraries(yt-lib-yson_helpers PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ cpp-yson-node
+ library-yql-utils
+ yql-utils-log
+ providers-yt-common
+)
+target_sources(yt-lib-yson_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..e67eddbb50
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yson_helpers)
+target_link_libraries(yt-lib-yson_helpers PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-yson
+ cpp-yson-node
+ library-yql-utils
+ yql-utils-log
+ providers-yt-common
+)
+target_sources(yt-lib-yson_helpers PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp
+)
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/lib/yt_download/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..597c4676e3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yt_download)
+target_link_libraries(yt-lib-yt_download PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ yql-core-file_storage
+ yql-utils-log
+ library-yql-utils
+ library-cpp-cgiparam
+ cpp-digest-md5
+)
+target_sources(yt-lib-yt_download PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..42230d8757
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yt_download)
+target_link_libraries(yt-lib-yt_download PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ yql-core-file_storage
+ yql-utils-log
+ library-yql-utils
+ library-cpp-cgiparam
+ cpp-digest-md5
+)
+target_sources(yt-lib-yt_download PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..42230d8757
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yt_download)
+target_link_libraries(yt-lib-yt_download PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ yql-core-file_storage
+ yql-utils-log
+ library-yql-utils
+ library-cpp-cgiparam
+ cpp-digest-md5
+)
+target_sources(yt-lib-yt_download PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.txt b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..597c4676e3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-lib-yt_download)
+target_link_libraries(yt-lib-yt_download PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-init_yt_api
+ yql-core-file_storage
+ yql-utils-log
+ library-yql-utils
+ library-cpp-cgiparam
+ cpp-digest-md5
+)
+target_sources(yt-lib-yt_download PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp
+)
diff --git a/ydb/library/yql/providers/yt/lib/yt_download/ya.make b/ydb/library/yql/providers/yt/lib/yt_download/ya.make
new file mode 100644
index 0000000000..508baa01d0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+SRCS(
+ yt_download.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/providers/yt/lib/init_yt_api
+ ydb/library/yql/core/file_storage
+ ydb/library/yql/utils/log
+ ydb/library/yql/utils
+ library/cpp/cgiparam
+ library/cpp/digest/md5
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp b/ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp
new file mode 100644
index 0000000000..77e88f2110
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/yt_download.cpp
@@ -0,0 +1,111 @@
+#include "yt_download.h"
+
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/init_yt_api/init.h>
+#include <ydb/library/yql/core/file_storage/defs/provider.h>
+
+#include <ydb/library/yql/utils/md5_stream.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/interface/client.h>
+
+#include <library/cpp/cgiparam/cgiparam.h>
+
+#include <util/string/cast.h>
+#include <util/stream/input.h>
+#include <util/stream/file.h>
+#include <util/system/fstat.h>
+
+namespace NYql {
+
+class TYtDownloader: public NYql::NFS::IDownloader {
+public:
+ TYtDownloader(const TFileStorageConfig& /*config*/, const TString& defaultServer)
+ : DefaultServer_(defaultServer)
+ {
+ }
+ ~TYtDownloader() = default;
+
+ bool Accept(const THttpURL& url) final {
+ const auto rawScheme = url.GetField(NUri::TField::FieldScheme);
+ return NUri::EqualNoCase(rawScheme, "yt");
+ }
+
+ std::tuple<NYql::NFS::TDataProvider, TString, TString> Download(const THttpURL& url, const TString& oauthToken, const TString& oldEtag, const TString& /*oldLastModified*/) final {
+ InitYtApiOnce();
+
+ TCgiParameters params(url.GetField(NUri::TField::FieldQuery));
+ NYT::TCreateClientOptions createOpts;
+ if (oauthToken) {
+ createOpts.Token(oauthToken);
+ }
+ auto host = url.PrintS(NUri::TField::FlagHostPort);
+ if (host == "current") {
+ if (!DefaultServer_) {
+ throw yexception() << "Cannot download url: " << url.PrintS() << ", default cluster is not defined";
+ }
+ host = DefaultServer_;
+ }
+ auto path = params.Has("path") ? params.Get("path") : TString{TStringBuf(url.GetField(NUri::TField::FieldPath)).Skip(1)};
+
+ auto client = NYT::CreateClient(host, createOpts);
+ NYT::IClientBasePtr tx = client;
+ TString txId = params.Get("transaction_id");
+ if (!txId) {
+ txId = params.Get("t");
+ }
+ YQL_LOG(INFO) << "YtDownload: host=" << host << ", path='" << path << "', tx=" << txId;
+
+ if (txId) {
+ TGUID g;
+ if (!GetGuid(txId, g)) {
+ throw yexception() << "Bad transaction ID: " << txId;
+ }
+ if (g) {
+ tx = client->AttachTransaction(g);
+ }
+ }
+
+ const NYT::TNode attrs = tx->Get(path + "/@", NYT::TGetOptions().AttributeFilter(
+ NYT::TAttributeFilter()
+ .AddAttribute(TString("revision"))
+ .AddAttribute(TString("content_revision"))
+ ));
+ auto rev = ToString(GetContentRevision(attrs));
+ if (oldEtag == rev) {
+ return std::make_tuple(NYql::NFS::TDataProvider{}, TString{}, TString{});
+ }
+
+ auto puller = [tx = std::move(tx), path = std::move(path)](const TFsPath& dstFile) -> std::pair<ui64, TString> {
+ auto reader = tx->CreateFileReader(path);
+
+ TFile outFile(dstFile, CreateAlways | ARW | AX);
+ TUnbufferedFileOutput out(outFile);
+ TMd5OutputStream md5Out(out);
+
+ const ui64 size = TransferData(reader.Get(), &md5Out);
+ auto result = std::make_pair(size, md5Out.Finalize());
+ out.Finish();
+ outFile.Close();
+
+ i64 dstFileLen = GetFileLength(dstFile.c_str());
+ if (dstFileLen == -1) {
+ ythrow TSystemError() << "cannot get file length: " << dstFile;
+ }
+
+ YQL_ENSURE(static_cast<ui64>(dstFileLen) == size);
+ return result;
+ };
+ return std::make_tuple(puller, rev, TString{});
+ }
+
+private:
+ const TString DefaultServer_;
+};
+
+NYql::NFS::IDownloaderPtr MakeYtDownloader(const TFileStorageConfig& config, const TString& defaultServer) {
+ return MakeIntrusive<TYtDownloader>(config, defaultServer);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/yt_download/yt_download.h b/ydb/library/yql/providers/yt/lib/yt_download/yt_download.h
new file mode 100644
index 0000000000..e16ec14635
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yt_download/yt_download.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <ydb/library/yql/core/file_storage/defs/downloader.h>
+
+namespace NYql {
+
+class TFileStorageConfig;
+
+NYql::NFS::IDownloaderPtr MakeYtDownloader(const TFileStorageConfig& config, const TString& defaultServer = {});
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/opt/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/opt/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..4fb42d76bd
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-opt)
+target_compile_options(providers-yt-opt PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-opt PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-row_spec
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-ast
+)
+target_sources(providers-yt-opt PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp
+)
diff --git a/ydb/library/yql/providers/yt/opt/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/opt/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..5d8fc62ec0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-opt)
+target_compile_options(providers-yt-opt PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-opt PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-row_spec
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-ast
+)
+target_sources(providers-yt-opt PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp
+)
diff --git a/ydb/library/yql/providers/yt/opt/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/opt/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..5d8fc62ec0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-opt)
+target_compile_options(providers-yt-opt PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-opt PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-row_spec
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-ast
+)
+target_sources(providers-yt-opt PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp
+)
diff --git a/ydb/library/yql/providers/yt/opt/CMakeLists.txt b/ydb/library/yql/providers/yt/opt/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/opt/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/opt/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..4fb42d76bd
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(providers-yt-opt)
+target_compile_options(providers-yt-opt PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-opt PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-lib-row_spec
+ yql-core-expr_nodes
+ library-yql-core
+ library-yql-ast
+)
+target_sources(providers-yt-opt PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/ydb/library/yql/providers/yt/provider/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..6c40d166a9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,113 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-provider)
+target_compile_options(providers-yt-provider PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-provider PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-cpp-disjoint_sets
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-extract_predicate
+ yql-public-udf
+ public-udf-tz
+ library-yql-sql
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ core-issue-protos
+ yql-core-peephole_opt
+ yql-core-type_ann
+ yql-core-file_storage
+ yql-dq-integration
+ yql-dq-opt
+ library-yql-minikql
+ providers-common-codec
+ providers-common-config
+ providers-common-dq
+ providers-common-mkql
+ providers-common-proto
+ providers-common-activation
+ providers-common-provider
+ common-schema-expr
+ providers-common-transform
+ providers-dq-common
+ providers-dq-expr_nodes
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-lib-expr_traits
+ yt-lib-graph_reorder
+ yt-lib-hash
+ yt-lib-key_filter
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-row_spec
+ yt-lib-skiff
+ yt-lib-yson_helpers
+ providers-yt-opt
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-provider PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_key.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp
+)
+generate_enum_serilization(providers-yt-provider
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+)
diff --git a/ydb/library/yql/providers/yt/provider/CMakeLists.linux-aarch64.txt b/ydb/library/yql/providers/yt/provider/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..b4dbad2e59
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,114 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-provider)
+target_compile_options(providers-yt-provider PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-provider PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-cpp-disjoint_sets
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-extract_predicate
+ yql-public-udf
+ public-udf-tz
+ library-yql-sql
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ core-issue-protos
+ yql-core-peephole_opt
+ yql-core-type_ann
+ yql-core-file_storage
+ yql-dq-integration
+ yql-dq-opt
+ library-yql-minikql
+ providers-common-codec
+ providers-common-config
+ providers-common-dq
+ providers-common-mkql
+ providers-common-proto
+ providers-common-activation
+ providers-common-provider
+ common-schema-expr
+ providers-common-transform
+ providers-dq-common
+ providers-dq-expr_nodes
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-lib-expr_traits
+ yt-lib-graph_reorder
+ yt-lib-hash
+ yt-lib-key_filter
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-row_spec
+ yt-lib-skiff
+ yt-lib-yson_helpers
+ providers-yt-opt
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-provider PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_key.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp
+)
+generate_enum_serilization(providers-yt-provider
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+)
diff --git a/ydb/library/yql/providers/yt/provider/CMakeLists.linux-x86_64.txt b/ydb/library/yql/providers/yt/provider/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..b4dbad2e59
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,114 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-provider)
+target_compile_options(providers-yt-provider PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-provider PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-cpp-disjoint_sets
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-extract_predicate
+ yql-public-udf
+ public-udf-tz
+ library-yql-sql
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ core-issue-protos
+ yql-core-peephole_opt
+ yql-core-type_ann
+ yql-core-file_storage
+ yql-dq-integration
+ yql-dq-opt
+ library-yql-minikql
+ providers-common-codec
+ providers-common-config
+ providers-common-dq
+ providers-common-mkql
+ providers-common-proto
+ providers-common-activation
+ providers-common-provider
+ common-schema-expr
+ providers-common-transform
+ providers-dq-common
+ providers-dq-expr_nodes
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-lib-expr_traits
+ yt-lib-graph_reorder
+ yt-lib-hash
+ yt-lib-key_filter
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-row_spec
+ yt-lib-skiff
+ yt-lib-yson_helpers
+ providers-yt-opt
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-provider PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_key.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp
+)
+generate_enum_serilization(providers-yt-provider
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+)
diff --git a/ydb/library/yql/providers/yt/provider/CMakeLists.txt b/ydb/library/yql/providers/yt/provider/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/ydb/library/yql/providers/yt/provider/CMakeLists.windows-x86_64.txt b/ydb/library/yql/providers/yt/provider/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..6c40d166a9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,113 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(providers-yt-provider)
+target_compile_options(providers-yt-provider PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(providers-yt-provider PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yson-node
+ library-cpp-disjoint_sets
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ library-yql-ast
+ yql-core-extract_predicate
+ yql-public-udf
+ public-udf-tz
+ library-yql-sql
+ library-yql-utils
+ yql-utils-log
+ library-yql-core
+ yql-core-expr_nodes
+ yql-core-issue
+ core-issue-protos
+ yql-core-peephole_opt
+ yql-core-type_ann
+ yql-core-file_storage
+ yql-dq-integration
+ yql-dq-opt
+ library-yql-minikql
+ providers-common-codec
+ providers-common-config
+ providers-common-dq
+ providers-common-mkql
+ providers-common-proto
+ providers-common-activation
+ providers-common-provider
+ common-schema-expr
+ providers-common-transform
+ providers-dq-common
+ providers-dq-expr_nodes
+ providers-result-expr_nodes
+ providers-stat-expr_nodes
+ providers-yt-common
+ providers-yt-expr_nodes
+ yt-lib-expr_traits
+ yt-lib-graph_reorder
+ yt-lib-hash
+ yt-lib-key_filter
+ yt-lib-mkql_helpers
+ yt-lib-res_pull
+ yt-lib-row_spec
+ yt-lib-skiff
+ yt-lib-yson_helpers
+ providers-yt-opt
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(providers-yt-provider PRIVATE
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_key.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp
+)
+generate_enum_serilization(providers-yt-provider
+ ${CMAKE_SOURCE_DIR}/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+ INCLUDE_HEADERS
+ ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
+)
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..26f9bed5e1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/ut/ya.make
@@ -0,0 +1,34 @@
+UNITTEST_FOR(ydb/library/yql/providers/yt/provider)
+
+SIZE(SMALL)
+
+SRCS(
+ yql_yt_dq_integration_ut.cpp
+ yql_yt_epoch_ut.cpp
+ yql_yt_cbo_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_cbo_ut.cpp b/ydb/library/yql/providers/yt/provider/ut/yql_yt_cbo_ut.cpp
new file mode 100644
index 0000000000..485ca16842
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/ut/yql_yt_cbo_ut.cpp
@@ -0,0 +1,137 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_join_impl.h>
+
+namespace NYql {
+
+namespace {
+
+TExprNode::TPtr MakeLabel(const std::vector<TString>& labelStrings, TExprContext& ctx) {
+ TVector<TExprNodePtr> label; label.reserve(labelStrings.size());
+
+ auto position = ctx.AppendPosition({});
+ for (auto& str : labelStrings) {
+ label.emplace_back(ctx.NewAtom(position, str));
+ }
+
+ return Build<TCoAtomList>(ctx, position)
+ .Add(label)
+ .Done()
+ .Ptr();
+}
+
+TYtJoinNodeOp::TPtr MakeOp(const std::vector<TString>& leftLabel, const std::vector<TString>& rightLabel, TVector<TString>&& scope, TExprContext& ctx) {
+ auto op = MakeIntrusive<TYtJoinNodeOp>();
+ auto position = ctx.AppendPosition({});
+ op->LeftLabel = MakeLabel(leftLabel, ctx);
+ op->RightLabel = MakeLabel(rightLabel, ctx);
+ op->JoinKind = ctx.NewAtom(position, "Inner");
+ op->Scope = std::move(scope);
+ return op;
+}
+
+TYtJoinNodeLeaf::TPtr MakeLeaf(const std::vector<TString>& label, TVector<TString>&& scope, ui64 rows, ui64 size, TExprContext& ctx) {
+ // fake section
+ auto position = ctx.AppendPosition({});
+
+ auto section = Build<TYtSection>(ctx, position)
+ .Paths().Build()
+ .Settings()
+ .Add().Name().Build("Test").Value<TCoAtom>().Build("1").Build()
+ .Add().Name().Build("Rows").Value<TCoAtom>().Build(ToString(rows)).Build()
+ .Add().Name().Build("Size").Value<TCoAtom>().Build(ToString(size)).Build()
+ .Build()
+ .Done();
+
+ auto leaf = MakeIntrusive<TYtJoinNodeLeaf>(section, TMaybeNode<TCoLambda>{});
+ if (label.size() == 1) {
+ leaf->Label = ctx.NewAtom(position, label.front());
+ } else {
+ leaf->Label = MakeLabel(label, ctx);
+ }
+ leaf->Scope = std::move(scope);
+ return leaf;
+}
+
+} // namespace
+
+Y_UNIT_TEST_SUITE(TYqlCBO) {
+
+Y_UNIT_TEST(OrderJoinsDoesNothingWhenCBODisabled) {
+ TYtState::TPtr state = MakeIntrusive<TYtState>();
+ TYtJoinNodeOp::TPtr tree = nullptr;
+ TYtJoinNodeOp::TPtr optimizedTree;
+ state->Configuration->CostBasedOptimizer = ECostBasedOptimizer::Disable;
+
+ TExprContext ctx;
+
+ optimizedTree = OrderJoins(tree, state, ctx);
+ UNIT_ASSERT_VALUES_EQUAL(tree, optimizedTree);
+}
+
+Y_UNIT_TEST(OrderJoins2Tables) {
+ TExprContext exprCtx;
+ auto tree = MakeOp({"c", "c_nationkey"}, {"n", "n_nationkey"}, {"c", "n"}, exprCtx);
+ tree->Left = MakeLeaf({"c"}, {"c"}, 1000000, 1233333, exprCtx);
+ tree->Right = MakeLeaf({"n"}, {"n"}, 10000, 12333, exprCtx);
+
+ TYtState::TPtr state = MakeIntrusive<TYtState>();
+ state->Configuration->CostBasedOptimizer = ECostBasedOptimizer::PG;
+ auto optimizedTree = OrderJoins(tree, state, exprCtx, true);
+ UNIT_ASSERT(optimizedTree != tree);
+ UNIT_ASSERT(optimizedTree->Left);
+ UNIT_ASSERT(optimizedTree->Right);
+ UNIT_ASSERT(optimizedTree->LeftLabel);
+ UNIT_ASSERT(optimizedTree->RightLabel);
+ UNIT_ASSERT(optimizedTree->JoinKind);
+ UNIT_ASSERT(optimizedTree->LeftLabel->ChildrenSize() == 2);
+ UNIT_ASSERT(optimizedTree->RightLabel->ChildrenSize() == 2);
+ UNIT_ASSERT_VALUES_EQUAL("c", optimizedTree->LeftLabel->Child(0)->Content());
+ UNIT_ASSERT_VALUES_EQUAL("c_nationkey", optimizedTree->LeftLabel->Child(1)->Content());
+ UNIT_ASSERT_VALUES_EQUAL("n", optimizedTree->RightLabel->Child(0)->Content());
+ UNIT_ASSERT_VALUES_EQUAL("n_nationkey", optimizedTree->RightLabel->Child(1)->Content());
+}
+
+Y_UNIT_TEST(OrderJoins2TablesComplexLabel)
+{
+ TExprContext exprCtx;
+ auto tree = MakeOp({"c", "c_nationkey"}, {"n", "n_nationkey"}, {"c", "n", "e"}, exprCtx);
+ tree->Left = MakeLeaf({"c"}, {"c"}, 1000000, 1233333, exprCtx);
+ tree->Right = MakeLeaf({"n"}, {"n", "e"}, 10000, 12333, exprCtx);
+
+ TYtState::TPtr state = MakeIntrusive<TYtState>();
+ state->Configuration->CostBasedOptimizer = ECostBasedOptimizer::PG;
+ auto optimizedTree = OrderJoins(tree, state, exprCtx, true);
+ UNIT_ASSERT(optimizedTree != tree);
+}
+
+Y_UNIT_TEST(OrderJoins2TablesTableIn2Rels)
+{
+ TExprContext exprCtx;
+ auto tree = MakeOp({"c", "c_nationkey"}, {"n", "n_nationkey"}, {"c", "n", "e"}, exprCtx);
+ tree->Left = MakeLeaf({"c"}, {"c"}, 1000000, 1233333, exprCtx);
+ tree->Right = MakeLeaf({"n"}, {"n", "c"}, 10000, 12333, exprCtx);
+
+ TYtState::TPtr state = MakeIntrusive<TYtState>();
+ state->Configuration->CostBasedOptimizer = ECostBasedOptimizer::PG;
+ auto optimizedTree = OrderJoins(tree, state, exprCtx, true);
+ UNIT_ASSERT(optimizedTree != tree);
+}
+
+Y_UNIT_TEST(UnsupportedJoin)
+{
+ TExprContext exprCtx;
+ auto tree = MakeOp({"c", "c_nationkey"}, {"n", "n_nationkey"}, {"c", "n"}, exprCtx);
+ tree->Left = MakeLeaf({"c"}, {"c"}, 1000000, 1233333, exprCtx);
+ tree->Right = MakeLeaf({"n"}, {"n"}, 10000, 12333, exprCtx);
+ tree->JoinKind = exprCtx.NewAtom(exprCtx.AppendPosition({}), "Left");
+
+ TYtState::TPtr state = MakeIntrusive<TYtState>();
+ state->Configuration->CostBasedOptimizer = ECostBasedOptimizer::PG;
+ auto optimizedTree = OrderJoins(tree, state, exprCtx, true);
+ UNIT_ASSERT(optimizedTree == tree);
+}
+
+} // Y_UNIT_TEST_SUITE(TYqlCBO)
+
+} // namespace NYql
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..a6973aca2a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/ya.make
@@ -0,0 +1,101 @@
+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_join_reorder.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..af6ff96473
--- /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()) {
+ TConstraintNode::TSetOfSetsType 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()) {
+ TConstraintNode::TSetOfSetsType 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..327b066d47
--- /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 = GetTablesTmpFolder(*config);
+ 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..bbb187db66
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp
@@ -0,0 +1,850 @@
+#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();
+ TExprNode::TPtr selfRead;
+ if (tableName == TStringBuf("self")) {
+ selfRead = newReadNode;
+ } else if (tableName == TStringBuf("self_raw")) {
+ selfRead = rightOverRead;
+ } else {
+ YQL_ENSURE(false, "Unknown table name (should be self or self_raw): " << tableName);
+ }
+
+ if (read.World().Ptr()->Type() == TExprNode::World) {
+ return selfRead;
+ }
+
+ return ctx.Builder(node->Pos())
+ .Callable("Right!")
+ .Callable(0, "Cons!")
+ .Add(0, read.World().Ptr())
+ .Add(1, selfRead)
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ // 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..c8cb525da2
--- /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::TContentType(distinct->GetContent())));
+ 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..32b40de6b1
--- /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));
+ }
+
+ if (canFallback) {
+ throw TFallbackError(MakeIntrusive<TIssue>(std::move(issue))) << message;
+ } else {
+ ctx.IssueManager.RaiseIssue(issue);
+ 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..e341a942c4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp
@@ -0,0 +1,2141 @@
+#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 (auto maybeCons = TMaybeNode<TCoRight>(&lambdaBody).Input().Maybe<TCoCons>()) {
+ syncList.emplace(maybeCons.Cast().World().Ptr(), syncList.size());
+ return IsYtIsolatedLambdaImpl(maybeCons.Cast().Input().Ref(), syncList, usedCluster, supportsReads, supportsDq, visited);
+ }
+
+ 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;
+ }
+
+ if (auto right = TMaybeNode<TCoRight>(node)) {
+ auto cons = right.Cast().Input().Maybe<TCoCons>();
+ if (cons) {
+ remaps[node.Get()] = cons.Cast().Input().Ptr();
+ return true;
+ }
+ }
+
+ 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, TYqlRowSpecInfo::TPtr outRowSpec, 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;
+ const ui64 outNativeYtTypeFlags = outRowSpec ? outRowSpec->GetNativeYtTypeFlags() : (state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ TYtOutTableInfo outTable(scheme.Cast<TStructExprType>(), outNativeYtTypeFlags);
+ TMaybe<NYT::TNode> outNativeType;
+ if (outRowSpec) {
+ outNativeType = outRowSpec->GetNativeYtType();
+ }
+ bool first = !outRowSpec;
+ 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) {
+ outNativeType = currentNativeType;
+ first = false;
+ }
+ const bool needTableMap = pathInfo.RequiresRemap() || bool(sysColumns)
+ || outTable.RowSpec->GetNativeYtTypeFlags() != pathInfo.GetNativeYtTypeFlags()
+ || currentNativeType != outNativeType;
+ 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 && outNativeType) {
+ outTable.RowSpec->CopyTypeOrders(*outNativeType);
+ }
+ 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>(), outNativeYtTypeFlags);
+ if (outNativeType) {
+ mapOutTable.RowSpec->CopyTypeOrders(*outNativeType);
+ }
+ 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..ebb0f6c91c
--- /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, TYqlRowSpecInfo::TPtr outRowSpec, 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..9487b35af4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp
@@ -0,0 +1,4762 @@
+#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/cbo/cbo_optimizer.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..3680c38a5a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.h
@@ -0,0 +1,68 @@
+#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);
+TYtJoinNodeOp::TPtr OrderJoins(TYtJoinNodeOp::TPtr op, const TYtState::TPtr& state, TExprContext& ctx, bool debug = false);
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp
new file mode 100644
index 0000000000..777a9bace6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_join_reorder.cpp
@@ -0,0 +1,346 @@
+#include "yql_yt_join_impl.h"
+#include "yql_yt_helpers.h"
+
+#include <ydb/library/yql/parser/pg_wrapper/interface/optimizer.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+
+namespace NYql {
+
+namespace {
+
+bool AreSimilarTrees(TYtJoinNode::TPtr node1, TYtJoinNode::TPtr node2) {
+ if (node1 == node2) {
+ return true;
+ }
+ if (node1 && !node2) {
+ return false;
+ }
+ if (node2 && !node1) {
+ return false;
+ }
+ if (node1->Scope != node2->Scope) {
+ return false;
+ }
+ auto opLeft = dynamic_cast<TYtJoinNodeOp*>(node1.Get());
+ auto opRight = dynamic_cast<TYtJoinNodeOp*>(node2.Get());
+ if (opLeft && opRight) {
+ return AreSimilarTrees(opLeft->Left, opRight->Left)
+ && AreSimilarTrees(opLeft->Right, opRight->Right);
+ } else if (!opLeft && !opRight) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void DebugPrint(TYtJoinNode::TPtr node, TExprContext& ctx, int level) {
+ auto* op = dynamic_cast<TYtJoinNodeOp*>(node.Get());
+ auto printScope = [](const TVector<TString>& scope) -> TString {
+ TStringBuilder b;
+ for (auto& s : scope) {
+ b << s << ",";
+ }
+ return b;
+ };
+ TString prefix;
+ for (int i = 0; i < level; i++) {
+ prefix += ' ';
+ }
+ if (op) {
+ Cerr << prefix
+ << "Op: "
+ << "Type: " << NCommon::ExprToPrettyString(ctx, *op->JoinKind)
+ << "Left: " << NCommon::ExprToPrettyString(ctx, *op->LeftLabel)
+ << "Right: " << NCommon::ExprToPrettyString(ctx, *op->RightLabel)
+ << "Scope: " << printScope(op->Scope) << "\n"
+ << "\n";
+ DebugPrint(op->Left, ctx, level+1);
+ DebugPrint(op->Right, ctx, level+1);
+ } else {
+ auto* leaf = dynamic_cast<TYtJoinNodeLeaf*>(node.Get());
+ Cerr << prefix
+ << "Leaf: "
+ << "Section: " << NCommon::ExprToPrettyString(ctx, *leaf->Section.Ptr())
+ << "Label: " << NCommon::ExprToPrettyString(ctx, *leaf->Label)
+ << "Scope: " << printScope(leaf->Scope) << "\n"
+ << "\n";
+ }
+}
+
+class TJoinReorderer {
+public:
+ TJoinReorderer(
+ TYtJoinNodeOp::TPtr op,
+ const TYtState::TPtr& state,
+ TExprContext& ctx,
+ bool debug = false)
+ : Root(op)
+ , State(state)
+ , Ctx(ctx)
+ , Debug(debug)
+ {
+ Y_UNUSED(State);
+
+ if (Debug) {
+ DebugPrint(Root, Ctx, 0);
+ }
+ }
+
+ TYtJoinNodeOp::TPtr Do() {
+ CollectRels(Root);
+ if (!CollectOps(Root)) {
+ return Root;
+ }
+
+ IOptimizer::TInput input;
+ input.EqClasses = std::move(EqClasses);
+ input.Rels = std::move(Rels);
+ input.Normalize();
+ if (Debug) {
+ Cerr << "Input: " << input.ToString();
+ }
+
+ std::function<void(const TString& str)> log;
+
+ if (Debug) {
+ log = [](const TString& str) {
+ Cerr << str << "\n";
+ };
+ }
+
+ std::unique_ptr<IOptimizer> opt = std::unique_ptr<IOptimizer>(MakePgOptimizer(input, log));
+
+ Result = opt->JoinSearch();
+
+ if (Debug) {
+ Cerr << "Result: " << Result.ToString() << "\n";
+ }
+
+ TVector<TString> scope;
+ TYtJoinNodeOp::TPtr res = dynamic_cast<TYtJoinNodeOp*>(Convert(0, scope).Get());
+
+ YQL_ENSURE(res);
+ DebugPrint(res, Ctx, 0);
+
+ if (Debug) {
+ DebugPrint(res, Ctx, 0);
+ }
+
+ return res;
+ }
+
+private:
+ int GetVarId(int relId, TStringBuf column) {
+ int varId = 0;
+ auto maybeVarId = VarIds[relId-1].find(column);
+ if (maybeVarId != VarIds[relId-1].end()) {
+ varId = maybeVarId->second;
+ } else {
+ varId = Rels[relId - 1].TargetVars.size() + 1;
+ VarIds[relId - 1][column] = varId;
+ Rels[relId - 1].TargetVars.emplace_back();
+ Var2TableCol[relId - 1].emplace_back();
+ }
+ return varId;
+ }
+
+ void ExtractVars(auto& vars, TExprNode::TPtr labels) {
+ for (ui32 i = 0; i < labels->ChildrenSize(); i += 2) {
+ auto table = labels->Child(i)->Content();
+ auto column = labels->Child(i + 1)->Content();
+
+ const auto& relIds = Table2RelIds[table];
+ YQL_ENSURE(!relIds.empty());
+
+ for (int relId : relIds) {
+ int varId = GetVarId(relId, column);
+
+ vars.emplace_back(std::make_tuple(relId, varId, table, column));
+ }
+ }
+ };
+
+ std::vector<TStringBuf> GetTables(TExprNode::TPtr label)
+ {
+ if (label->ChildrenSize() == 0) {
+ return {label->Content()};
+ } else {
+ std::vector<TStringBuf> tables;
+ tables.reserve(label->ChildrenSize());
+ for (ui32 i = 0; i < label->ChildrenSize(); i++) {
+ tables.emplace_back(label->Child(i)->Content());
+ }
+ return tables;
+ }
+ }
+
+ void OnLeaf(TYtJoinNodeLeaf* leaf) {
+ int relId = Rels.size() + 1;
+ Rels.emplace_back(IOptimizer::TRel{});
+ Var2TableCol.emplace_back();
+ // rel -> varIds
+ VarIds.emplace_back(THashMap<TStringBuf, int>{});
+ // rel -> tables
+ RelTables.emplace_back(std::vector<TStringBuf>{});
+ for (const auto& table : GetTables(leaf->Label)) {
+ RelTables.back().emplace_back(table);
+ Table2RelIds[table].emplace_back(relId);
+ }
+ auto& rel = Rels[relId - 1];
+
+ TYtSection section{leaf->Section};
+ if (Y_UNLIKELY(!section.Settings().Empty())) {
+ // ut
+ if (Y_UNLIKELY(section.Settings().Item(0).Name() == "Test")) {
+ for (const auto& setting : section.Settings()) {
+ if (setting.Name() == "Rows") {
+ rel.Rows += FromString<ui64>(setting.Value().Ref().Content());
+ } else if (setting.Name() == "Size") {
+ rel.TotalCost += FromString<ui64>(setting.Value().Ref().Content());
+ }
+ }
+ }
+ } else {
+ for (auto path: section.Paths()) {
+ auto stat = TYtTableBaseInfo::GetStat(path.Table());
+ rel.TotalCost += stat->DataSize;
+ rel.Rows += stat->RecordsCount;
+ }
+ }
+
+ int leafIndex = relId - 1;
+ if (leafIndex >= static_cast<int>(Leafs.size())) {
+ Leafs.resize(leafIndex + 1);
+ }
+ Leafs[leafIndex] = leaf;
+ };
+
+ bool OnOp(TYtJoinNodeOp* op) {
+#define CHECK(A, B) \
+ if (Y_UNLIKELY(!(A))) { \
+ TIssues issues; \
+ issues.AddIssue(TIssue(B).SetCode(0, NYql::TSeverityIds::S_INFO)); \
+ Ctx.IssueManager.AddIssues(issues); \
+ return false; \
+ }
+
+ CHECK(op->JoinKind->Content() == "Inner", "Unsupported join type");
+ CHECK(!op->Output, "Non empty output");
+ CHECK(op->StarOptions.empty(), "Non empty StarOptions");
+
+ CHECK(op->LeftLabel->ChildrenSize() == 2, "Only 1 var per join supported");
+ CHECK(op->RightLabel->ChildrenSize() == 2, "Only 1 var per join supported");
+
+ // relId, varId, table, column
+ std::vector<std::tuple<int,int,TStringBuf,TStringBuf>> vars;
+ ExtractVars(vars, op->LeftLabel);
+ ExtractVars(vars, op->RightLabel);
+
+ IOptimizer::TEq eqClass;
+
+ for (auto& [relId, varId, table, column] : vars) {
+ eqClass.Vars.emplace_back(std::make_tuple(relId, varId));
+ Var2TableCol[relId - 1][varId - 1] = std::make_tuple(table, column);
+ }
+
+ EqClasses.emplace_back(std::move(eqClass));
+
+#undef CHECK
+ return true;
+ }
+
+ bool CollectOps(TYtJoinNode::TPtr node)
+ {
+ if (auto* op = dynamic_cast<TYtJoinNodeOp*>(node.Get())) {
+ return OnOp(op)
+ && CollectOps(op->Left)
+ && CollectOps(op->Right);
+ }
+ return true;
+ }
+
+ void CollectRels(TYtJoinNode::TPtr node)
+ {
+ if (auto* op = dynamic_cast<TYtJoinNodeOp*>(node.Get())) {
+ CollectRels(op->Left);
+ CollectRels(op->Right);
+ } else if (auto* leaf = dynamic_cast<TYtJoinNodeLeaf*>(node.Get())) {
+ OnLeaf(leaf);
+ }
+ }
+
+ TExprNode::TPtr MakeLabel(IOptimizer::TVarId var) const {
+ auto [relId, varId] = var;
+ auto [table, column] = Var2TableCol[relId - 1][varId - 1];
+
+ TVector<TExprNodePtr> label = {
+ Ctx.NewAtom(Root->JoinKind->Pos(), table),
+ Ctx.NewAtom(Root->JoinKind->Pos(), column)
+ };
+
+ return Build<TCoAtomList>(Ctx, Root->JoinKind->Pos())
+ .Add(label)
+ .Done()
+ .Ptr();
+ }
+
+ TYtJoinNode::TPtr Convert(int nodeId, TVector<TString>& scope) const
+ {
+ const IOptimizer::TJoinNode* node = &Result.Nodes[nodeId];
+ if (node->Outer == -1 && node->Inner == -1) {
+ YQL_ENSURE(node->Rels.size() == 1);
+ auto leaf = Leafs[node->Rels[0]-1];
+ YQL_ENSURE(leaf);
+ YQL_ENSURE(!leaf->Scope.empty());
+ scope.insert(scope.end(), leaf->Scope.begin(), leaf->Scope.end());
+ return leaf;
+ } else if (node->Outer != -1 && node->Inner != -1) {
+ auto ret = MakeIntrusive<TYtJoinNodeOp>();
+ YQL_ENSURE(node->Mode == IOptimizer::EJoinType::Inner, "Unsupported join type");
+ ret->JoinKind = Ctx.NewAtom(Root->JoinKind->Pos(), "Inner");
+ ret->LeftLabel = MakeLabel(node->LeftVar);
+ ret->RightLabel = MakeLabel(node->RightVar);
+ int index = scope.size();
+ ret->Left = Convert(node->Outer, scope);
+ ret->Right = Convert(node->Inner, scope);
+ ret->Scope.insert(ret->Scope.end(), scope.begin() + index, scope.end());
+ return ret;
+ } else {
+ YQL_ENSURE(false, "Wrong CBO node");
+ }
+ }
+
+ TYtJoinNodeOp::TPtr Root;
+ const TYtState::TPtr& State;
+ TExprContext& Ctx;
+ bool Debug;
+
+ THashMap<TStringBuf, std::vector<int>> Table2RelIds;
+ std::vector<IOptimizer::TRel> Rels;
+ std::vector<std::vector<TStringBuf>> RelTables;
+ std::vector<TYtJoinNodeLeaf*> Leafs;
+ std::vector<std::vector<std::tuple<TStringBuf, TStringBuf>>> Var2TableCol;
+
+ std::vector<THashMap<TStringBuf, int>> VarIds;
+
+ std::vector<IOptimizer::TEq> EqClasses;
+
+ IOptimizer::TOutput Result;
+};
+
+} // namespace
+
+TYtJoinNodeOp::TPtr OrderJoins(TYtJoinNodeOp::TPtr op, const TYtState::TPtr& state, TExprContext& ctx, bool debug)
+{
+ if (state->Configuration->CostBasedOptimizer.Get().GetOrElse(ECostBasedOptimizer::Disable) == ECostBasedOptimizer::Disable) {
+ return op;
+ }
+
+ auto result = TJoinReorderer(op, state, ctx, debug).Do();
+ if (!debug && AreSimilarTrees(result, op)) {
+ return op;
+ }
+ return result;
+}
+
+} // namespace NYql
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..a60603cb43
--- /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 = TString("@").append(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..05676e48e5
--- /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(), *State_->Types->RandomProvider)) {
+ 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(), *State_->Types->RandomProvider)) {
+ 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..21fc6c1e14
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp
@@ -0,0 +1,2661 @@
+#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<false>));
+ AddHandler(0, &TCoTopSort::Match, HNDL(SortOverAlreadySorted<true>));
+ 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);
+ }
+
+ template<bool IsTop>
+ TMaybeNode<TExprBase> SortOverAlreadySorted(TExprBase node, TExprContext& ctx) const {
+ const auto sort = node.Cast<std::conditional_t<IsTop, TCoTopBase, 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;
+ }
+ }
+
+ if constexpr (IsTop)
+ return Build<TCoTake>(ctx, sort.Pos())
+ .Input(list)
+ .Count(sort.Count())
+ .Done();
+ else
+ 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..3a5c073365
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp
@@ -0,0 +1,537 @@
+#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,
+ size_t timeout)
+{
+ 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 = GetTablesTmpFolder(*state->Configuration);
+ 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));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral(timeout));
+ 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;
+ size_t timeout = state->Configuration->DQRPCReaderTimeout.Get(cluster).GetOrElse(DEFAULT_RPC_READER_TIMEOUT).MilliSeconds();
+ 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, timeout);
+ else
+ return BuildDqYtInputCall<true>(outputType, inputItemType, cluster, tokenName, ytRead.Input(), state, ctx, isRPC, timeout);
+ }
+
+ 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..a77f046af4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp
@@ -0,0 +1,676 @@
+#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, TYqlRowSpecInfo::TPtr outRowSpec, 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(),
+ outRowSpec,
+ 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, TYqlRowSpecInfo::TPtr outRowSpec, 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, outRowSpec, 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, TYqlRowSpecInfo::TPtr outRowSpec, 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, outRowSpec, 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..b7d3a29e55
--- /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,
+ TYqlRowSpecInfo::TPtr outRowSpec, 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..f0fccd2241
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp
@@ -0,0 +1,2559 @@
+#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)) {
+ if (sorted = sorted->FilterFields(ctx, [&columns](const TConstraintNode::TPathType& path) { return !path.empty() && columns.contains(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(outTable.RowSpec()));
+ }
+ mapOut.SetUnique(distinct, map.Mapper().Pos(), ctx);
+ mapOut.RowSpec->SetConstraints(outTable.Ref().GetConstraintSet());
+
+ 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);
+ mergeOut.RowSpec->SetConstraints(outTable.Ref().GetConstraintSet());
+
+ 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);
+ auto outRowSpec = MakeIntrusive<TYqlRowSpecInfo>(op.Output().Item(0).RowSpec());
+ const bool sortedMerge = TYtMerge::Match(node.Get()) && outRowSpec->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(),
+ outRowSpec,
+ 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;
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ 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());
+ auto mapper = Build<TCoLambda>(ctx, out.Pos())
+ .Args({"stream"})
+ .Body("stream")
+ .Done().Ptr();
+
+ if (!unordered && outRowSpec->IsSorted()) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered), TNodeFlags::Default)
+ .Build()
+ .Build();
+ TKeySelectorBuilder builder(out.Pos(), ctx, useNativeDescSort, outRowSpec->GetType());
+ builder.ProcessRowSpec(*outRowSpec);
+ if (builder.NeedMap()) {
+ mapper = builder.MakeRemapLambda(true);
+ }
+ }
+ 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(mapper)
+ .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..8ada8dc400
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp
@@ -0,0 +1,7414 @@
+#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<false>));
+ AddHandler(0, &TCoTopSort::Match, HNDL(Sort<true>));
+ 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, &TYtOutputOpBase::Match, HNDL(TableContentWithSettings));
+ AddHandler(1, &TYtOutputOpBase::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();
+ }
+
+ template<bool IsTop>
+ TMaybeNode<TExprBase> Sort(TExprBase node, TExprContext& ctx) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ const auto sort = node.Cast<std::conditional_t<IsTop, TCoTopBase, 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().template Maybe<TCoRight>().Input().template 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().template 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().template 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().template 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), TNodeFlags::Default)
+ .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());
+
+ TConvertInputOpts opts;
+ if (useExplicitColumns) {
+ opts.ExplicitFields(*sortOut.RowSpec, node.Pos(), ctx);
+ }
+
+ auto res = canUseMerge ?
+ TExprBase(Build<TYtMerge>(ctx, node.Pos())
+ .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), TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Build()
+ .Done()):
+ TExprBase(Build<TYtSort>(ctx, node.Pos())
+ .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()
+ .Done());
+
+ res = Build<TYtOutput>(ctx, node.Pos())
+ .Operation(res)
+ .OutIndex().Value(0U).Build()
+ .Done();
+
+
+ if constexpr (IsTop) {
+ res = Build<TCoTake>(ctx, node.Pos())
+ .Input(res)
+ .Count(sort.Count())
+ .Done();
+ }
+
+ return res;
+ }
+
+ 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(0U).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<TCoTopSort>(ctx, write.Pos())
+ .Input(writeOutput)
+ .Count(topSort.Count().Cast())
+ .SortDirections(topSort.SortDirections().Cast())
+ .KeySelectorLambda(topSort.KeySelectorLambda().Cast())
+ .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, *State_->Types);
+ 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;
+ }
+
+ TYtDSource dataSource = GetDataSource(ytRead, ctx);
+ if (!State_->Configuration->_EnableYtPartitioning.Get(dataSource.Cluster().StringValue()).GetOrElse(false)) {
+ 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 tryReorder = State_->Configuration->CostBasedOptimizer.Get().GetOrElse(ECostBasedOptimizer::Disable) != ECostBasedOptimizer::Disable
+ && equiJoin.Input().Size() > 2
+ && HasOnlyOneJoinType(*equiJoin.Joins().Ptr(), "Inner");
+
+ const bool waitAllInputs = State_->Configuration->JoinWaitAllInputs.Get().GetOrElse(false) || tryReorder;
+
+ 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);
+ if (tryReorder) {
+ const auto optimizedTree = OrderJoins(tree, State_, ctx);
+ if (optimizedTree != tree) {
+ return ExportYtEquiJoin(equiJoin, *optimizedTree, ctx, State_);
+ }
+ }
+ 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;
+ TProcessedNodesSet processedNodes;
+ VisitExpr(res, [&nodesToOptimize, &processedNodes](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())) {
+ processedNodes.insert(input->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ if (nodesToOptimize.empty()) {
+ return node;
+ }
+
+ TSyncMap syncList;
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &processedNodes; // Prevent optimizer to go deeper than current operation
+ 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, settings);
+
+ 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;
+ TProcessedNodesSet processedNodes;
+ VisitExpr(res, [&nodesToOptimize, &processedNodes](const TExprNode::TPtr& input) {
+ if (TYtTableContent::Match(input.Get())) {
+ nodesToOptimize.insert(input.Get());
+ return false;
+ }
+ if (TYtOutput::Match(input.Get())) {
+ processedNodes.insert(input->UniqueId());
+ 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);
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &processedNodes; // Prevent optimizer to go deeper than current operation
+ 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, settings);
+
+ 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;
+ }
+
+ TYqlRowSpecInfo::TPtr outRowSpec;
+ bool keepSortness = false;
+ if (op.Maybe<TYtReduce>()) {
+ keepSortness = true;
+ } else if (op.Maybe<TYtCopy>() || op.Maybe<TYtMerge>()) {
+ outRowSpec = MakeIntrusive<TYqlRowSpecInfo>(op.Output().Item(0).RowSpec());
+ keepSortness = outRowSpec->IsSorted();
+ } else if (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(), outRowSpec, 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..ec89019eb5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp
@@ -0,0 +1,508 @@
+#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, IRandomProvider& randomProvider) {
+ 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, randomProvider)) {
+ 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, IRandomProvider& randomProvider) {
+ return TYtTableDescriptionBase::FillViews(TString{YtProviderName}, cluster, table, Meta->Attrs, ctx, moduleResolver, randomProvider);
+}
+
+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) {
+ std::unordered_set<std::string_view> groups;
+ if (ytState->Types->Credentials != nullptr) {
+ groups.insert(ytState->Types->Credentials->GetGroups().begin(), ytState->Types->Credentials->GetGroups().end());
+ }
+ auto filter = [userName, ytState, groups = std::move(groups)](const NYql::TAttr& attr) -> bool {
+ if (!attr.HasActivation()) {
+ return true;
+ }
+ if (NConfig::Allow(attr.GetActivation(), userName, groups)) {
+ 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..ead2fab597
--- /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, IRandomProvider& randomProvider);
+ 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, IRandomProvider& randomProvider);
+};
+
+// 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..aac4d392ea
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp
@@ -0,0 +1,2991 @@
+#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());
+}
+
+TYtTableStatInfo::TPtr TYtTableBaseInfo::GetStat(NNodes::TExprBase node) {
+ TExprNode::TPtr statNode;
+ if (node.Maybe<TYtOutTable>()) {
+ statNode = node.Cast<TYtOutTable>().Stat().Ptr();
+ } else if (node.Maybe<TYtTable>()) {
+ statNode = node.Cast<TYtTable>().Stat().Ptr();
+ } else if (node.Maybe<TYtOutput>()) {
+ auto tableWithCluster = GetOutTableWithCluster(node);
+ statNode = tableWithCluster.first.Cast<TYtOutTable>().Stat().Ptr();
+ } else {
+ ythrow yexception() << "Not a table node " << (node.Raw() ? TString{node.Ref().Content()}.Quote() : TStringBuf("\"null\""));
+ }
+ return MakeIntrusive<TYtTableStatInfo>(statNode);
+}
+
+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 = static_cast<const TDistinctConstraintNode*>(distinct->OnlySimpleColumns(ctx))) {
+ auto content = simple->GetContent();
+ if (1U < content.size()) {
+ std::unordered_set<std::string_view> sorted(RowSpec->SortMembers.cbegin(), RowSpec->SortMembers.cend());
+ if (const auto filtered = simple->FilterFields(ctx, [&sorted](const TConstraintNode::TPathType& path) { return 1U == path.size() && sorted.contains(path.front()); }))
+ content = filtered->GetContent();
+ }
+ std::vector<std::string_view> uniques(content.front().size());
+ std::transform(content.front().cbegin(), content.front().cend(), uniques.begin(), [&](const TConstraintNode::TSetType& set) { return set.front().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..b4291c5b06
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table.h
@@ -0,0 +1,353 @@
+#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 TYtTableStatInfo::TPtr GetStat(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..0aad683f34
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp
@@ -0,0 +1,398 @@
+#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, IRandomProvider& randomProvider)
+{
+ 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;
+ settings.FileAliasPrefix = "view_" + randomProvider.GenUuid4().AsGuidString() + "/";
+ 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 {};
+ }
+
+ constexpr TStringBuf OuterFuncs[] = {
+ "SecureParam",
+ "CurrentOperationId",
+ "CurrentOperationSharedId",
+ "CurrentAuthenticatedUser",
+ };
+
+ constexpr TStringBuf CodegenFuncs[] = {
+ "FilePath",
+ "FileContent",
+ "FolderPath",
+ "Files",
+ "Configure!",
+ };
+
+ bool hasError = false;
+ VisitExpr(*exprRoot, [&](const TExprNode& node) {
+ for (const auto& name : OuterFuncs) {
+ if (node.IsCallable(name)) {
+ hasError = true;
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << name << " function can't be used in views"));
+ return false;
+ }
+ }
+
+ 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()) {
+ for (const auto& name : OuterFuncs) {
+ if (node.Head().Head().Content() == name) {
+ hasError = true;
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << name << " function can't be used in views"));
+ return false;
+ }
+ }
+
+ for (const auto& name : CodegenFuncs) {
+ if (node.Head().Head().Content() == name) {
+ hasError = true;
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << name << " function can't be used inside generated code 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, IRandomProvider& randomProvider)
+{
+ Sql = sql;
+ CompiledSql = CompileViewSql(provider, cluster, sql, syntaxVersion, ctx, moduleResolver, randomProvider);
+ 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, IRandomProvider& randomProvider)
+{
+ // (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, randomProvider)) {
+ return false;
+ }
+
+ if (viewSql) {
+ if (!View) {
+ if (!View.ConstructInPlace().Fill(provider, cluster, viewSql, syntaxVersion, ctx, moduleResolver, randomProvider)) {
+ 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, IRandomProvider& randomProvider)
+{
+ 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, randomProvider)) {
+ 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..58432d3e97
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <library/cpp/random_provider/random_provider.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, IRandomProvider& randomProvider);
+ 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, IRandomProvider& randomProvider);
+ void CleanupCompiledSQL();
+ bool FillViews(const TString& provider, const TString& cluster, const TString& table, const THashMap<TString, TString>& metaAttrs,
+ TExprContext& ctx, IModuleResolver* moduleResolver, IRandomProvider& randomProvider);
+};
+
+}
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/yt/CMakeLists.txt b/yt/CMakeLists.txt
new file mode 100644
index 0000000000..f7ed775592
--- /dev/null
+++ b/yt/CMakeLists.txt
@@ -0,0 +1,12 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(cpp)
+add_subdirectory(yql)
+add_subdirectory(yt)
+add_subdirectory(yt_proto)
diff --git a/yt/cpp/CMakeLists.txt b/yt/cpp/CMakeLists.txt
new file mode 100644
index 0000000000..20efbba0c6
--- /dev/null
+++ b/yt/cpp/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(mapreduce)
diff --git a/yt/cpp/mapreduce/CMakeLists.txt b/yt/cpp/mapreduce/CMakeLists.txt
new file mode 100644
index 0000000000..9906c343df
--- /dev/null
+++ b/yt/cpp/mapreduce/CMakeLists.txt
@@ -0,0 +1,16 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(client)
+add_subdirectory(common)
+add_subdirectory(http)
+add_subdirectory(interface)
+add_subdirectory(io)
+add_subdirectory(library)
+add_subdirectory(raw_client)
+add_subdirectory(skiff)
diff --git a/yt/cpp/mapreduce/client/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/client/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..9554a16585
--- /dev/null
+++ b/yt/cpp/mapreduce/client/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,70 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-client)
+target_compile_options(cpp-mapreduce-client PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-client PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-digest-md5
+ library-cpp-sighandler
+ cpp-threading-blocking_queue
+ cpp-threading-future
+ library-cpp-type_info
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ mapreduce-library-table_schema
+ cpp-mapreduce-raw_client
+ yt-yt-core
+ yt-core-http
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/abortable_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/batch_request_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/format_hints.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/init.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_preparer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/prepare_operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/py_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retry_heavy_write_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryful_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryless_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction_pinger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/yt_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/dummy_job_profiler.cpp
+)
+generate_enum_serilization(cpp-mapreduce-client
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/client/structured_table_formats.h
+)
diff --git a/yt/cpp/mapreduce/client/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/client/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..44ce5a6103
--- /dev/null
+++ b/yt/cpp/mapreduce/client/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,70 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-client)
+target_compile_options(cpp-mapreduce-client PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-client PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-digest-md5
+ library-cpp-sighandler
+ cpp-threading-blocking_queue
+ cpp-threading-future
+ library-cpp-type_info
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ mapreduce-library-table_schema
+ cpp-mapreduce-raw_client
+ yt_proto-yt-core
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/abortable_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/batch_request_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/format_hints.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/init.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_preparer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/prepare_operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/py_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retry_heavy_write_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryful_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryless_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction_pinger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/yt_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/dummy_job_profiler.cpp
+)
+generate_enum_serilization(cpp-mapreduce-client
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/client/structured_table_formats.h
+)
diff --git a/yt/cpp/mapreduce/client/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/client/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..969fd94697
--- /dev/null
+++ b/yt/cpp/mapreduce/client/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,71 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-client)
+target_compile_options(cpp-mapreduce-client PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-client PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-digest-md5
+ library-cpp-sighandler
+ cpp-threading-blocking_queue
+ cpp-threading-future
+ library-cpp-type_info
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ mapreduce-library-table_schema
+ cpp-mapreduce-raw_client
+ yt-yt-core
+ yt-core-http
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/abortable_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/batch_request_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/format_hints.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/init.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_preparer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/prepare_operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/py_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retry_heavy_write_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryful_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryless_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction_pinger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/yt_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/dummy_job_profiler.cpp
+)
+generate_enum_serilization(cpp-mapreduce-client
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/client/structured_table_formats.h
+)
diff --git a/yt/cpp/mapreduce/client/CMakeLists.txt b/yt/cpp/mapreduce/client/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/client/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/client/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/client/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..1a4f73e137
--- /dev/null
+++ b/yt/cpp/mapreduce/client/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,67 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-client)
+target_link_libraries(cpp-mapreduce-client PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-digest-md5
+ library-cpp-sighandler
+ cpp-threading-blocking_queue
+ cpp-threading-future
+ library-cpp-type_info
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ cpp-mapreduce-io
+ mapreduce-library-table_schema
+ cpp-mapreduce-raw_client
+ yt-yt-core
+ yt-core-http
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/abortable_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/batch_request_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/file_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/format_hints.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/init.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_preparer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/prepare_operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/py_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retry_heavy_write_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryful_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/retryless_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/skiff.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/transaction_pinger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/yt_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/dummy_job_profiler.cpp
+)
+generate_enum_serilization(cpp-mapreduce-client
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/client/structured_table_formats.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/client/structured_table_formats.h
+)
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/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/common/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..5955826356
--- /dev/null
+++ b/yt/cpp/mapreduce/common/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,31 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-common)
+target_compile_options(cpp-mapreduce-common PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-common PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-json
+ library-cpp-svnversion
+ cpp-threading-future
+ library-cpp-yson
+ cpp-yson-json
+ cpp-yson-node
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+)
+target_sources(cpp-mapreduce-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/debug_metrics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/retry_lib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/wait_proxy.cpp
+)
diff --git a/yt/cpp/mapreduce/common/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/common/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..9b588d74bd
--- /dev/null
+++ b/yt/cpp/mapreduce/common/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-common)
+target_compile_options(cpp-mapreduce-common PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-common PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-json
+ library-cpp-svnversion
+ cpp-threading-future
+ library-cpp-yson
+ cpp-yson-json
+ cpp-yson-node
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+)
+target_sources(cpp-mapreduce-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/debug_metrics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/retry_lib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/wait_proxy.cpp
+)
diff --git a/yt/cpp/mapreduce/common/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/common/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..9b588d74bd
--- /dev/null
+++ b/yt/cpp/mapreduce/common/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-common)
+target_compile_options(cpp-mapreduce-common PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-common PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-json
+ library-cpp-svnversion
+ cpp-threading-future
+ library-cpp-yson
+ cpp-yson-json
+ cpp-yson-node
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+)
+target_sources(cpp-mapreduce-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/debug_metrics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/retry_lib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/wait_proxy.cpp
+)
diff --git a/yt/cpp/mapreduce/common/CMakeLists.txt b/yt/cpp/mapreduce/common/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/common/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/common/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/common/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..dbe9c55c3d
--- /dev/null
+++ b/yt/cpp/mapreduce/common/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-common)
+target_link_libraries(cpp-mapreduce-common PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-json
+ library-cpp-svnversion
+ cpp-threading-future
+ library-cpp-yson
+ cpp-yson-json
+ cpp-yson-node
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+)
+target_sources(cpp-mapreduce-common PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/debug_metrics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/retry_lib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/common/wait_proxy.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/http/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..8212c9d8d1
--- /dev/null
+++ b/yt/cpp/mapreduce/http/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,37 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-http)
+target_compile_options(cpp-mapreduce-http PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-http PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+ cpp-http-io
+ cpp-string_utils-base64
+ cpp-string_utils-quote
+ cpp-threading-cron
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt-core-http
+ yt-core-https
+)
+target_sources(cpp-mapreduce-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/abortable_http_response.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/host_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/retry_request.cpp
+)
diff --git a/yt/cpp/mapreduce/http/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/http/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..b199382910
--- /dev/null
+++ b/yt/cpp/mapreduce/http/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-http)
+target_compile_options(cpp-mapreduce-http PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-http PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+ cpp-http-io
+ cpp-string_utils-base64
+ cpp-string_utils-quote
+ cpp-threading-cron
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt-core-http
+ yt-core-https
+)
+target_sources(cpp-mapreduce-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/abortable_http_response.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/host_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/retry_request.cpp
+)
diff --git a/yt/cpp/mapreduce/http/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/http/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..b199382910
--- /dev/null
+++ b/yt/cpp/mapreduce/http/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-http)
+target_compile_options(cpp-mapreduce-http PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-http PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+ cpp-http-io
+ cpp-string_utils-base64
+ cpp-string_utils-quote
+ cpp-threading-cron
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt-core-http
+ yt-core-https
+)
+target_sources(cpp-mapreduce-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/abortable_http_response.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/host_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/retry_request.cpp
+)
diff --git a/yt/cpp/mapreduce/http/CMakeLists.txt b/yt/cpp/mapreduce/http/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/http/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/http/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/http/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..b742e11f63
--- /dev/null
+++ b/yt/cpp/mapreduce/http/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,34 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-http)
+target_link_libraries(cpp-mapreduce-http PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-deprecated-atomic
+ cpp-http-io
+ cpp-string_utils-base64
+ cpp-string_utils-quote
+ cpp-threading-cron
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt-core-http
+ yt-core-https
+)
+target_sources(cpp-mapreduce-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/abortable_http_response.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/host_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/http_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/http/retry_request.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/interface/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..f6db27793d
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,143 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(logging)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-interface)
+target_compile_options(cpp-mapreduce-interface PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-interface PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-type_info
+ cpp-threading-future
+ cpp-yson-node
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ yt-library-tvm
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-interface PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/errors.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/io.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/skiff_row.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/tvm.cpp
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client_method_options.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/common.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/config.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/cypress.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_counters.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_statistics.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/operation.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/protobuf_format.h
+)
diff --git a/yt/cpp/mapreduce/interface/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/interface/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..36491da7b1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,144 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(logging)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-interface)
+target_compile_options(cpp-mapreduce-interface PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-interface PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-type_info
+ cpp-threading-future
+ cpp-yson-node
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ yt-library-tvm
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-interface PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/errors.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/io.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/skiff_row.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/tvm.cpp
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client_method_options.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/common.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/config.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/cypress.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_counters.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_statistics.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/operation.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/protobuf_format.h
+)
diff --git a/yt/cpp/mapreduce/interface/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/interface/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..36491da7b1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,144 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(logging)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-interface)
+target_compile_options(cpp-mapreduce-interface PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-interface PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-type_info
+ cpp-threading-future
+ cpp-yson-node
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ yt-library-tvm
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-interface PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/errors.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/io.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/skiff_row.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/tvm.cpp
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client_method_options.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/common.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/config.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/cypress.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_counters.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_statistics.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/operation.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/protobuf_format.h
+)
diff --git a/yt/cpp/mapreduce/interface/CMakeLists.txt b/yt/cpp/mapreduce/interface/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/interface/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/interface/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..24f64a0c99
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,140 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(logging)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(cpp-mapreduce-interface)
+target_link_libraries(cpp-mapreduce-interface PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-type_info
+ cpp-threading-future
+ cpp-yson-node
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ yt-library-tvm
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(cpp-mapreduce-interface PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/errors.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/io.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/skiff_row.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/tvm.cpp
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client_method_options.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client_method_options.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/client.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/client.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/common.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/common.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/config.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/config.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/cypress.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/cypress.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_counters.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_counters.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/job_statistics.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/job_statistics.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/operation.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/operation.h
+)
+generate_enum_serilization(cpp-mapreduce-interface
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/protobuf_format.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/protobuf_format.h
+)
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/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/interface/logging/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..031665ae24
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,34 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(mapreduce-interface-logging)
+target_compile_options(mapreduce-interface-logging PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(mapreduce-interface-logging PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-logging
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(mapreduce-interface-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/yt_log.cpp
+)
+generate_enum_serilization(mapreduce-interface-logging
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/logging/logger.h
+)
diff --git a/yt/cpp/mapreduce/interface/logging/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/interface/logging/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..7fa0ca41ef
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,35 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(mapreduce-interface-logging)
+target_compile_options(mapreduce-interface-logging PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(mapreduce-interface-logging PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-logging
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(mapreduce-interface-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/yt_log.cpp
+)
+generate_enum_serilization(mapreduce-interface-logging
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/logging/logger.h
+)
diff --git a/yt/cpp/mapreduce/interface/logging/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/interface/logging/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..7fa0ca41ef
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,35 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(mapreduce-interface-logging)
+target_compile_options(mapreduce-interface-logging PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(mapreduce-interface-logging PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-logging
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(mapreduce-interface-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/yt_log.cpp
+)
+generate_enum_serilization(mapreduce-interface-logging
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/logging/logger.h
+)
diff --git a/yt/cpp/mapreduce/interface/logging/CMakeLists.txt b/yt/cpp/mapreduce/interface/logging/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/interface/logging/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/interface/logging/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..7801fa5db6
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,31 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_enum_parser_bin
+ TOOL_enum_parser_dependency
+ tools/enum_parser/enum_parser
+ enum_parser
+)
+
+add_library(mapreduce-interface-logging)
+target_link_libraries(mapreduce-interface-logging PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-logging
+ tools-enum_parser-enum_serialization_runtime
+)
+target_sources(mapreduce-interface-logging PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/yt_log.cpp
+)
+generate_enum_serilization(mapreduce-interface-logging
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/interface/logging/logger.h
+ INCLUDE_HEADERS
+ yt/cpp/mapreduce/interface/logging/logger.h
+)
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/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/io/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..b448828312
--- /dev/null
+++ b/yt/cpp/mapreduce/io/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,41 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-io)
+target_compile_options(cpp-mapreduce-io PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-io PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ cpp-yson-node
+ cpp-mapreduce-skiff
+)
+target_sources(cpp-mapreduce-io PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/counting_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/lenval_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_row_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/stream_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_writer.cpp
+)
diff --git a/yt/cpp/mapreduce/io/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/io/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..3ccc1d4f98
--- /dev/null
+++ b/yt/cpp/mapreduce/io/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-io)
+target_compile_options(cpp-mapreduce-io PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-io PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ cpp-yson-node
+ cpp-mapreduce-skiff
+)
+target_sources(cpp-mapreduce-io PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/counting_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/lenval_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_row_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/stream_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_writer.cpp
+)
diff --git a/yt/cpp/mapreduce/io/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/io/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..3ccc1d4f98
--- /dev/null
+++ b/yt/cpp/mapreduce/io/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,42 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-io)
+target_compile_options(cpp-mapreduce-io PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-io PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ cpp-yson-node
+ cpp-mapreduce-skiff
+)
+target_sources(cpp-mapreduce-io PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/counting_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/lenval_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_row_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/stream_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_writer.cpp
+)
diff --git a/yt/cpp/mapreduce/io/CMakeLists.txt b/yt/cpp/mapreduce/io/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/io/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/io/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/io/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..53a0290cda
--- /dev/null
+++ b/yt/cpp/mapreduce/io/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,38 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-io)
+target_link_libraries(cpp-mapreduce-io PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-yson
+ cpp-mapreduce-common
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ yt_proto-yt-formats
+ cpp-yson-node
+ cpp-mapreduce-skiff
+)
+target_sources(cpp-mapreduce-io PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/counting_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/job_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/lenval_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/node_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/proto_table_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_row_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/skiff_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/stream_raw_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_reader.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/io/yamr_table_writer.cpp
+)
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/CMakeLists.txt b/yt/cpp/mapreduce/library/CMakeLists.txt
new file mode 100644
index 0000000000..bc51bb2b47
--- /dev/null
+++ b/yt/cpp/mapreduce/library/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(table_schema)
diff --git a/yt/cpp/mapreduce/library/table_schema/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/library/table_schema/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..92eec36f23
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(mapreduce-library-table_schema)
+target_compile_options(mapreduce-library-table_schema PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(mapreduce-library-table_schema PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-interface
+)
+target_sources(mapreduce-library-table_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/library/table_schema/protobuf.cpp
+)
diff --git a/yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..0bb5208c97
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(mapreduce-library-table_schema)
+target_compile_options(mapreduce-library-table_schema PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(mapreduce-library-table_schema PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-interface
+)
+target_sources(mapreduce-library-table_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/library/table_schema/protobuf.cpp
+)
diff --git a/yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..0bb5208c97
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(mapreduce-library-table_schema)
+target_compile_options(mapreduce-library-table_schema PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(mapreduce-library-table_schema PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-interface
+)
+target_sources(mapreduce-library-table_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/library/table_schema/protobuf.cpp
+)
diff --git a/yt/cpp/mapreduce/library/table_schema/CMakeLists.txt b/yt/cpp/mapreduce/library/table_schema/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/library/table_schema/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/library/table_schema/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..f8288b61f4
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(mapreduce-library-table_schema)
+target_link_libraries(mapreduce-library-table_schema PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-interface
+)
+target_sources(mapreduce-library-table_schema PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/library/table_schema/protobuf.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/raw_client/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..a1596bb36e
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-raw_client)
+target_compile_options(cpp-mapreduce-raw_client PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-raw_client PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ cpp-yson-node
+)
+target_sources(cpp-mapreduce-raw_client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp
+)
diff --git a/yt/cpp/mapreduce/raw_client/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/raw_client/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..66b3a60dd3
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-raw_client)
+target_compile_options(cpp-mapreduce-raw_client PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-raw_client PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ cpp-yson-node
+)
+target_sources(cpp-mapreduce-raw_client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp
+)
diff --git a/yt/cpp/mapreduce/raw_client/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/raw_client/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..66b3a60dd3
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-raw_client)
+target_compile_options(cpp-mapreduce-raw_client PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(cpp-mapreduce-raw_client PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ cpp-yson-node
+)
+target_sources(cpp-mapreduce-raw_client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp
+)
diff --git a/yt/cpp/mapreduce/raw_client/CMakeLists.txt b/yt/cpp/mapreduce/raw_client/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/raw_client/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/raw_client/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..ebb5269098
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,24 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-raw_client)
+target_link_libraries(cpp-mapreduce-raw_client PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-mapreduce-common
+ cpp-mapreduce-http
+ cpp-mapreduce-interface
+ mapreduce-interface-logging
+ cpp-yson-node
+)
+target_sources(cpp-mapreduce-raw_client PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_batch_request.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/raw_requests.cpp
+ ${CMAKE_SOURCE_DIR}/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/yt/cpp/mapreduce/skiff/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..d66d5d8810
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,15 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-skiff INTERFACE)
+target_link_libraries(cpp-mapreduce-skiff INTERFACE
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-skiff
+)
diff --git a/yt/cpp/mapreduce/skiff/CMakeLists.linux-aarch64.txt b/yt/cpp/mapreduce/skiff/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..7e33de6fb4
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,16 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-skiff INTERFACE)
+target_link_libraries(cpp-mapreduce-skiff INTERFACE
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-skiff
+)
diff --git a/yt/cpp/mapreduce/skiff/CMakeLists.linux-x86_64.txt b/yt/cpp/mapreduce/skiff/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..7e33de6fb4
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,16 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-skiff INTERFACE)
+target_link_libraries(cpp-mapreduce-skiff INTERFACE
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-skiff
+)
diff --git a/yt/cpp/mapreduce/skiff/CMakeLists.txt b/yt/cpp/mapreduce/skiff/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/cpp/mapreduce/skiff/CMakeLists.windows-x86_64.txt b/yt/cpp/mapreduce/skiff/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..d66d5d8810
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,15 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-mapreduce-skiff INTERFACE)
+target_link_libraries(cpp-mapreduce-skiff INTERFACE
+ contrib-libs-cxxsupp
+ yutil
+ library-cpp-skiff
+)
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/gradle.inc b/yt/gradle.inc
new file mode 100644
index 0000000000..44be1f45b4
--- /dev/null
+++ b/yt/gradle.inc
@@ -0,0 +1,4 @@
+MAVEN_GROUP_ID(tech.ytsaurus)
+
+ENABLE(GRADLE_EXPORT_PUBLISHING)
+ENABLE(SOURCES_JAR)
diff --git a/yt/opensource.inc b/yt/opensource.inc
new file mode 100644
index 0000000000..1e9cc193b2
--- /dev/null
+++ b/yt/opensource.inc
@@ -0,0 +1,21 @@
+IF (OPENSOURCE)
+ IF (MODULE_KIND == PROTO_LIBRARY)
+ EXCLUDE_TAGS(PY_PROTO)
+ ENDIF()
+
+ IF (MODULE_KIND == PY23_LIBRARY)
+ EXCLUDE_TAGS(PY2)
+ ENDIF()
+
+ IF (MODULE_KIND == PY23_TEST)
+ EXCLUDE_TAGS(PY2)
+ ENDIF()
+
+ RESTRICT_LICENSES(
+ DENY REQUIRE_DISCLOSURE FORBIDDEN PROTESTWARE
+ # https://st.yandex-team.ru/DTCC-553
+ EXCEPT contrib/libs/linux-headers
+ # CHYT
+ EXCEPT contrib/libs/fmt
+ )
+ENDIF()
diff --git a/yt/opensource_tests.inc b/yt/opensource_tests.inc
new file mode 100644
index 0000000000..734a41dea3
--- /dev/null
+++ b/yt/opensource_tests.inc
@@ -0,0 +1,24 @@
+IF (OPENSOURCE)
+ IF (MODULE_KIND == PROTO_LIBRARY)
+ EXCLUDE_TAGS(PY_PROTO)
+ ENDIF()
+
+ IF (MODULE_KIND == PY23_LIBRARY)
+ EXCLUDE_TAGS(PY2)
+ ENDIF()
+
+ IF (MODULE_KIND == PY23_TEST)
+ EXCLUDE_TAGS(PY2)
+ ENDIF()
+
+ RESTRICT_LICENSES(
+ DENY REQUIRE_DISCLOSURE FORBIDDEN PROTESTWARE
+ # https://st.yandex-team.ru/DTCC-553
+ EXCEPT contrib/libs/linux-headers
+ # CHYT
+ EXCEPT contrib/libs/fmt
+ # Java tests
+ EXCEPT contrib/java/junit/junit/4.13
+ EXCEPT contrib/java/javax/persistence/persistence-api/1.0
+ )
+ENDIF()
diff --git a/yt/ya_cpp.make.inc b/yt/ya_cpp.make.inc
new file mode 100644
index 0000000000..62c095d111
--- /dev/null
+++ b/yt/ya_cpp.make.inc
@@ -0,0 +1,2 @@
+# Common defaults for YT C++ projects
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
diff --git a/yt/yql/CMakeLists.txt b/yt/yql/CMakeLists.txt
new file mode 100644
index 0000000000..8180a508fa
--- /dev/null
+++ b/yt/yql/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(plugin)
diff --git a/yt/yql/plugin/CMakeLists.darwin-x86_64.txt b/yt/yql/plugin/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..e4e42a5389
--- /dev/null
+++ b/yt/yql/plugin/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(dynamic)
+add_subdirectory(native)
+
+add_library(yt-yql-plugin)
+target_link_libraries(yt-yql-plugin PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yql-plugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/plugin.cpp
+)
diff --git a/yt/yql/plugin/CMakeLists.linux-aarch64.txt b/yt/yql/plugin/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..3fb7f34a94
--- /dev/null
+++ b/yt/yql/plugin/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,20 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(dynamic)
+add_subdirectory(native)
+
+add_library(yt-yql-plugin)
+target_link_libraries(yt-yql-plugin PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yql-plugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/plugin.cpp
+)
diff --git a/yt/yql/plugin/CMakeLists.linux-x86_64.txt b/yt/yql/plugin/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..3fb7f34a94
--- /dev/null
+++ b/yt/yql/plugin/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,20 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(dynamic)
+add_subdirectory(native)
+
+add_library(yt-yql-plugin)
+target_link_libraries(yt-yql-plugin PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yql-plugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/plugin.cpp
+)
diff --git a/yt/yql/plugin/CMakeLists.txt b/yt/yql/plugin/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yql/plugin/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yql/plugin/CMakeLists.windows-x86_64.txt b/yt/yql/plugin/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..e4e42a5389
--- /dev/null
+++ b/yt/yql/plugin/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(dynamic)
+add_subdirectory(native)
+
+add_library(yt-yql-plugin)
+target_link_libraries(yt-yql-plugin PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yql-plugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/plugin.cpp
+)
diff --git a/yt/yql/plugin/bridge/interface.h b/yt/yql/plugin/bridge/interface.h
new file mode 100644
index 0000000000..062adb7ea5
--- /dev/null
+++ b/yt/yql/plugin/bridge/interface.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include <unistd.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This is a plain C version of an interface described in yt/yql/plugin/plugin.h.
+// All strings without separate length field are assumed to be null-terminated.
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBridgeYqlPluginOptions
+{
+ const char* MRJobBinary;
+ const char* UdfDirectory;
+
+ struct TBridgeCluster
+ {
+ const char* Cluster;
+ const char* Proxy;
+ };
+ ssize_t ClusterCount;
+ TBridgeCluster* Clusters;
+ const char* DefaultCluster;
+
+ const char* OperationAttributes;
+
+ const char* YTTokenPath;
+
+ // TODO(max42): passing C++ objects across shared libraries is incredibly
+ // fragile. This is a temporary mean until we come up with something more
+ // convenient; get rid of this ASAP.
+ using TLogBackendHolder = void;
+ TLogBackendHolder* LogBackend;
+};
+
+// Opaque type representing a YQL plugin.
+using TBridgeYqlPlugin = void;
+
+using TFuncBridgeCreateYqlPlugin = TBridgeYqlPlugin*(const TBridgeYqlPluginOptions* options);
+using TFuncBridgeFreeYqlPlugin = void(TBridgeYqlPlugin* plugin);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(max42): consider making structure an opaque type with accessors a-la
+// const char* BridgeGetYsonResult(const TBridgeQueryResult*). This would remove the need
+// to manually free string data.
+struct TBridgeQueryResult
+{
+ const char* YsonResult = nullptr;
+ ssize_t YsonResultLength = 0;
+ const char* Plan = nullptr;
+ ssize_t PlanLength = 0;
+ const char* Statistics = nullptr;
+ ssize_t StatisticsLength = 0;
+ const char* TaskInfo = nullptr;
+ ssize_t TaskInfoLength = 0;
+
+ const char* YsonError = nullptr;
+ ssize_t YsonErrorLength = 0;
+};
+
+using TFuncBridgeFreeQueryResult = void(TBridgeQueryResult* result);
+using TFuncBridgeRun = TBridgeQueryResult*(TBridgeYqlPlugin* plugin, const char* impersonationUser, const char* queryText, const char* settings);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define FOR_EACH_BRIDGE_INTERFACE_FUNCTION(XX) \
+ XX(BridgeCreateYqlPlugin) \
+ XX(BridgeFreeYqlPlugin) \
+ XX(BridgeFreeQueryResult) \
+ XX(BridgeRun)
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yql/plugin/bridge/plugin.cpp b/yt/yql/plugin/bridge/plugin.cpp
new file mode 100644
index 0000000000..52b9f92a01
--- /dev/null
+++ b/yt/yql/plugin/bridge/plugin.cpp
@@ -0,0 +1,120 @@
+#include "plugin.h"
+
+#include "interface.h"
+
+#include <yt/yql/plugin/plugin.h>
+#include <util/system/dynlib.h>
+
+#include <vector>
+#include <optional>
+
+namespace NYT::NYqlPlugin {
+namespace NBridge {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDynamicYqlPlugin
+{
+public:
+ TDynamicYqlPlugin(std::optional<TString> yqlPluginSharedLibrary)
+ {
+ const TString DefaultYqlPluginLibraryName = "./libyqlplugin.so";
+ auto sharedLibraryPath = yqlPluginSharedLibrary.value_or(DefaultYqlPluginLibraryName);
+ Library_.Open(sharedLibraryPath.data());
+ #define XX(function) function = reinterpret_cast<TFunc ## function*>(Library_.Sym(#function));
+ FOR_EACH_BRIDGE_INTERFACE_FUNCTION(XX);
+ #undef XX
+ }
+
+protected:
+ #define XX(function) TFunc ## function* function;
+ FOR_EACH_BRIDGE_INTERFACE_FUNCTION(XX)
+ #undef XX
+
+ TDynamicLibrary Library_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYqlPlugin
+ : public TDynamicYqlPlugin
+ , public IYqlPlugin
+{
+public:
+ explicit TYqlPlugin(TYqlPluginOptions& options)
+ : TDynamicYqlPlugin(options.YqlPluginSharedLibrary)
+ {
+ std::vector<TBridgeYqlPluginOptions::TBridgeCluster> bridgeClusters;
+ for (const auto& [cluster, proxy]: options.Clusters) {
+ bridgeClusters.push_back({
+ .Cluster = cluster.data(),
+ .Proxy = proxy.data(),
+ });
+ }
+
+ const char* operationAttributes = options.OperationAttributes
+ ? options.OperationAttributes.ToString().data()
+ : nullptr;
+
+ const char* defaultCluster = options.DefaultCluster
+ ? options.DefaultCluster->data()
+ : nullptr;
+
+ TBridgeYqlPluginOptions bridgeOptions {
+ .MRJobBinary = options.MRJobBinary.data(),
+ .UdfDirectory = options.UdfDirectory.data(),
+ .ClusterCount = static_cast<int>(bridgeClusters.size()),
+ .Clusters = bridgeClusters.data(),
+ .DefaultCluster = defaultCluster,
+ .OperationAttributes = operationAttributes,
+ .YTTokenPath = options.YTTokenPath.data(),
+ .LogBackend = &options.LogBackend,
+ };
+
+ BridgePlugin_ = BridgeCreateYqlPlugin(&bridgeOptions);
+ }
+
+ TQueryResult Run(TString impersonationUser, TString queryText, NYson::TYsonString settings) noexcept override
+ {
+ const char* settingsData = settings ? settings.ToString().data() : nullptr;
+ auto* bridgeQueryResult = BridgeRun(BridgePlugin_, impersonationUser.data(), queryText.data(), settingsData);
+ auto toString = [] (const char* str, size_t strLength) -> std::optional<TString> {
+ if (!str) {
+ return std::nullopt;
+ }
+ return TString(str, strLength);
+ };
+ TQueryResult queryResult = {
+ .YsonResult = toString(bridgeQueryResult->YsonResult, bridgeQueryResult->YsonResultLength),
+ .Plan = toString(bridgeQueryResult->Plan, bridgeQueryResult->PlanLength),
+ .Statistics = toString(bridgeQueryResult->Statistics, bridgeQueryResult->StatisticsLength),
+ .TaskInfo = toString(bridgeQueryResult->TaskInfo, bridgeQueryResult->TaskInfoLength),
+ .YsonError = toString(bridgeQueryResult->YsonError, bridgeQueryResult->YsonErrorLength),
+ };
+ BridgeFreeQueryResult(bridgeQueryResult);
+ return queryResult;
+ }
+
+ ~TYqlPlugin() override
+ {
+ BridgeFreeYqlPlugin(BridgePlugin_);
+ }
+
+private:
+ TBridgeYqlPlugin* BridgePlugin_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NBridge
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IYqlPlugin> CreateYqlPlugin(TYqlPluginOptions& options) noexcept
+{
+ return std::make_unique<NBridge::TYqlPlugin>(options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPlugin::NBridge
diff --git a/yt/yql/plugin/bridge/plugin.h b/yt/yql/plugin/bridge/plugin.h
new file mode 100644
index 0000000000..a80528e700
--- /dev/null
+++ b/yt/yql/plugin/bridge/plugin.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yql/plugin/plugin.h>
+
+namespace NYT::NYqlPlugin {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IYqlPlugin> CreateYqlPlugin(TYqlPluginOptions& options) noexcept;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPlugin
diff --git a/yt/yql/plugin/bridge/ya.make b/yt/yql/plugin/bridge/ya.make
new file mode 100644
index 0000000000..93425c5284
--- /dev/null
+++ b/yt/yql/plugin/bridge/ya.make
@@ -0,0 +1,11 @@
+LIBRARY()
+
+SRCS(
+ GLOBAL plugin.cpp
+)
+
+PEERDIR(
+ yt/yql/plugin
+)
+
+END()
diff --git a/yt/yql/plugin/dynamic/CMakeLists.darwin-x86_64.txt b/yt/yql/plugin/dynamic/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..bb76915330
--- /dev/null
+++ b/yt/yql/plugin/dynamic/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,31 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_shared_library(yqlplugin)
+target_link_libraries(yqlplugin PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-plugin-native
+)
+target_link_options(yqlplugin PRIVATE
+ -Wl,-platform_version,macos,11.0,11.0
+ -fPIC
+ -undefined
+ dynamic_lookup
+ -fPIC
+ -framework
+ CoreFoundation
+)
+target_sources(yqlplugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/impl.cpp
+)
+use_export_script(yqlplugin
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/dylib.exports
+)
+vcs_info(yqlplugin)
diff --git a/yt/yql/plugin/dynamic/CMakeLists.linux-aarch64.txt b/yt/yql/plugin/dynamic/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..63edf774f2
--- /dev/null
+++ b/yt/yql/plugin/dynamic/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,35 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_shared_library(yqlplugin)
+target_link_libraries(yqlplugin PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-plugin-native
+)
+target_link_options(yqlplugin PRIVATE
+ -ldl
+ -lrt
+ -Wl,--no-as-needed
+ -fPIC
+ -Wl,-z,notext
+ -fPIC
+ -lpthread
+ -lrt
+ -ldl
+ -lutil
+)
+target_sources(yqlplugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/impl.cpp
+)
+use_export_script(yqlplugin
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/dylib.exports
+)
+vcs_info(yqlplugin)
diff --git a/yt/yql/plugin/dynamic/CMakeLists.linux-x86_64.txt b/yt/yql/plugin/dynamic/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..63edf774f2
--- /dev/null
+++ b/yt/yql/plugin/dynamic/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,35 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_shared_library(yqlplugin)
+target_link_libraries(yqlplugin PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yql-plugin-native
+)
+target_link_options(yqlplugin PRIVATE
+ -ldl
+ -lrt
+ -Wl,--no-as-needed
+ -fPIC
+ -Wl,-z,notext
+ -fPIC
+ -lpthread
+ -lrt
+ -ldl
+ -lutil
+)
+target_sources(yqlplugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/impl.cpp
+)
+use_export_script(yqlplugin
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/dylib.exports
+)
+vcs_info(yqlplugin)
diff --git a/yt/yql/plugin/dynamic/CMakeLists.txt b/yt/yql/plugin/dynamic/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yql/plugin/dynamic/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yql/plugin/dynamic/CMakeLists.windows-x86_64.txt b/yt/yql/plugin/dynamic/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..2814417000
--- /dev/null
+++ b/yt/yql/plugin/dynamic/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_shared_library(yqlplugin)
+target_link_libraries(yqlplugin PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yql-plugin-native
+)
+target_sources(yqlplugin PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/impl.cpp
+)
+use_export_script(yqlplugin
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/dynamic/dylib.exports
+)
+vcs_info(yqlplugin)
diff --git a/yt/yql/plugin/dynamic/dylib.exports b/yt/yql/plugin/dynamic/dylib.exports
new file mode 100644
index 0000000000..ec1c95cb11
--- /dev/null
+++ b/yt/yql/plugin/dynamic/dylib.exports
@@ -0,0 +1,12 @@
+# YT <-> YQL bridge.
+BridgeCreateYqlPlugin
+BridgeFreeYqlPlugin
+BridgeFreeQueryResult
+BridgeRun
+
+# YQL <-> YQL UDFs interface.
+UdfAllocateWithSize
+UdfFreeWithSize
+UdfRegisterObject
+UdfTerminate
+UdfUnregisterObject
diff --git a/yt/yql/plugin/dynamic/impl.cpp b/yt/yql/plugin/dynamic/impl.cpp
new file mode 100644
index 0000000000..e39897c97e
--- /dev/null
+++ b/yt/yql/plugin/dynamic/impl.cpp
@@ -0,0 +1,93 @@
+#include <yt/yql/plugin/bridge/interface.h>
+#include <yt/yql/plugin/native/plugin.h>
+
+#include <type_traits>
+
+using namespace NYT::NYqlPlugin;
+using namespace NYT::NYson;
+
+extern "C" {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBridgeYqlPlugin* BridgeCreateYqlPlugin(const TBridgeYqlPluginOptions* bridgeOptions)
+{
+ THashMap<TString, TString> clusters;
+ for (auto clusterIndex = 0; clusterIndex < bridgeOptions->ClusterCount; ++clusterIndex) {
+ const auto& Cluster = bridgeOptions->Clusters[clusterIndex];
+ clusters[Cluster.Cluster] = Cluster.Proxy;
+ }
+
+ TYsonString operationAttributes = bridgeOptions->OperationAttributes
+ ? TYsonString(TString(bridgeOptions->OperationAttributes))
+ : TYsonString{};
+
+ TYqlPluginOptions options{
+ .MRJobBinary = TString(bridgeOptions->MRJobBinary),
+ .UdfDirectory = TString(bridgeOptions->UdfDirectory),
+ .Clusters = std::move(clusters),
+ .DefaultCluster = std::optional<TString>(bridgeOptions->DefaultCluster),
+ .OperationAttributes = operationAttributes,
+ .YTTokenPath = TString(bridgeOptions->YTTokenPath),
+ .LogBackend = std::move(*reinterpret_cast<THolder<TLogBackend>*>(bridgeOptions->LogBackend)),
+ };
+ auto nativePlugin = CreateYqlPlugin(options);
+ return nativePlugin.release();
+}
+
+void BridgeFreeYqlPlugin(TBridgeYqlPlugin* plugin)
+{
+ auto* nativePlugin = reinterpret_cast<IYqlPlugin*>(plugin);
+ delete nativePlugin;
+}
+
+void BridgeFreeQueryResult(TBridgeQueryResult* result)
+{
+ delete result->TaskInfo;
+ delete result->Statistics;
+ delete result->Plan;
+ delete result->YsonResult;
+ delete result->YsonError;
+ delete result;
+}
+
+TBridgeQueryResult* BridgeRun(TBridgeYqlPlugin* plugin, const char* impersonationUser, const char* queryText, const char* settings)
+{
+ static const TYsonString EmptyMap = TYsonString(TString("{}"));
+
+ auto* nativePlugin = reinterpret_cast<IYqlPlugin*>(plugin);
+ auto* bridgeResult = new TBridgeQueryResult;
+
+ auto fillString = [] (const char*& str, ssize_t& strLength, const std::optional<TString>& original) {
+ if (!original) {
+ str = nullptr;
+ strLength = 0;
+ return;
+ }
+ char* copy = new char[original->size() + 1];
+ memcpy(copy, original->data(), original->size() + 1);
+ str = copy;
+ strLength = original->size();
+ };
+
+ auto result = nativePlugin->Run(TString(impersonationUser), TString(queryText), settings ? TYsonString(TString(settings)) : EmptyMap);
+ fillString(bridgeResult->YsonResult, bridgeResult->YsonResultLength, result.YsonResult);
+ fillString(bridgeResult->Plan, bridgeResult->PlanLength, result.Plan);
+ fillString(bridgeResult->Statistics, bridgeResult->StatisticsLength, result.Statistics);
+ fillString(bridgeResult->TaskInfo, bridgeResult->TaskInfoLength, result.TaskInfo);
+ fillString(bridgeResult->YsonError, bridgeResult->YsonErrorLength, result.YsonError);
+
+ return bridgeResult;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Validate that the all functions from the bridge interface are implemented with proper signatures.
+
+#define XX(function) static_assert(std::is_same_v<decltype(&(function)), TFunc ## function*>);
+FOR_EACH_BRIDGE_INTERFACE_FUNCTION(XX)
+#undef XX
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // extern "C"
diff --git a/yt/yql/plugin/dynamic/ya.make b/yt/yql/plugin/dynamic/ya.make
new file mode 100644
index 0000000000..c91996acec
--- /dev/null
+++ b/yt/yql/plugin/dynamic/ya.make
@@ -0,0 +1,15 @@
+DLL(yqlplugin 1 0)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource.inc)
+
+EXPORTS_SCRIPT(dylib.exports)
+
+SRCS(
+ impl.cpp
+)
+
+PEERDIR(
+ yt/yql/plugin/native
+)
+
+END()
diff --git a/yt/yql/plugin/native/CMakeLists.darwin-x86_64.txt b/yt/yql/plugin/native/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..753bac36c8
--- /dev/null
+++ b/yt/yql/plugin/native/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,92 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-plugin-native)
+target_compile_options(yql-plugin-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/error_helpers.cpp
+)
+
+add_global_library_for(yql-plugin-native.global yql-plugin-native)
+target_compile_options(yql-plugin-native.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native.global PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/plugin.cpp
+)
diff --git a/yt/yql/plugin/native/CMakeLists.linux-aarch64.txt b/yt/yql/plugin/native/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..348d9ca192
--- /dev/null
+++ b/yt/yql/plugin/native/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,94 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-plugin-native)
+target_compile_options(yql-plugin-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/error_helpers.cpp
+)
+
+add_global_library_for(yql-plugin-native.global yql-plugin-native)
+target_compile_options(yql-plugin-native.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native.global PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/plugin.cpp
+)
diff --git a/yt/yql/plugin/native/CMakeLists.linux-x86_64.txt b/yt/yql/plugin/native/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..348d9ca192
--- /dev/null
+++ b/yt/yql/plugin/native/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,94 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-plugin-native)
+target_compile_options(yql-plugin-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/error_helpers.cpp
+)
+
+add_global_library_for(yql-plugin-native.global yql-plugin-native)
+target_compile_options(yql-plugin-native.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native.global PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/plugin.cpp
+)
diff --git a/yt/yql/plugin/native/CMakeLists.txt b/yt/yql/plugin/native/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yql/plugin/native/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yql/plugin/native/CMakeLists.windows-x86_64.txt b/yt/yql/plugin/native/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..753bac36c8
--- /dev/null
+++ b/yt/yql/plugin/native/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,92 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yql-plugin-native)
+target_compile_options(yql-plugin-native PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/error_helpers.cpp
+)
+
+add_global_library_for(yql-plugin-native.global yql-plugin-native)
+target_compile_options(yql-plugin-native.global PRIVATE
+ -DUSE_CURRENT_UDF_ABI_VERSION
+)
+target_link_libraries(yql-plugin-native.global PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+ library-cpp-resource
+ library-cpp-yson
+ cpp-yson-node
+ cpp-mapreduce-client
+ cpp-mapreduce-common
+ library-yql-ast
+ yql-sql-pg
+ yql-parser-pg_wrapper
+ yql-core-facade
+ yql-core-file_storage
+ core-file_storage-proto
+ core-file_storage-http_download
+ core-services-mounts
+ yql-core-user_data
+ library-yql-minikql
+ library-yql-protos
+ udf-service-exception_policy
+ yql-utils-backtrace
+ yql-utils-log
+ providers-common-proto
+ providers-common-udf_resolve
+ providers-solomon-gateway
+ providers-solomon-provider
+ yql-core-url_preprocessing
+ yt-gateway-native
+ yt-lib-log
+ yt-lib-yt_download
+ providers-yt-provider
+ yt-yql-plugin
+)
+target_sources(yql-plugin-native.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yql/plugin/native/plugin.cpp
+)
diff --git a/yt/yql/plugin/native/error_helpers.cpp b/yt/yql/plugin/native/error_helpers.cpp
new file mode 100644
index 0000000000..23b9fa5cba
--- /dev/null
+++ b/yt/yql/plugin/native/error_helpers.cpp
@@ -0,0 +1,121 @@
+#include "error_helpers.h"
+
+#include <library/cpp/yson/writer.h>
+
+#include <ydb/library/yql/public/issue/yql_issue_id.h>
+
+#include <ydb/library/yql/core/issue/protos/issue_id.pb.h>
+
+namespace NYT::NYqlPlugin {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const int IssueToErrorCodesShift = 30000;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ExceptionToYtErrorYson(const std::exception& exception)
+{
+ TStringStream yson;
+ ::NYson::TYsonWriter writer(&yson);
+
+ writer.OnBeginMap();
+ writer.OnKeyedItem("code");
+ writer.OnInt64Scalar(1); // Generic error
+ writer.OnKeyedItem("message");
+ writer.OnStringScalar(exception.what());
+ writer.OnKeyedItem("attributes");
+ writer.OnBeginMap();
+ writer.OnEndMap();
+
+ writer.OnEndMap();
+
+ return yson.Str();
+}
+
+TString IssuesToYtErrorYson(const NYql::TIssues& issues)
+{
+ TStringStream yson;
+ ::NYson::TYsonWriter writer(&yson);
+
+ auto serializePosition = [&] (const NYql::TPosition& position) {
+ writer.OnBeginMap();
+ {
+ writer.OnKeyedItem("column");
+ writer.OnInt64Scalar(position.Column);
+
+ writer.OnKeyedItem("row");
+ writer.OnInt64Scalar(position.Row);
+
+ if (!position.File.empty()) {
+ writer.OnKeyedItem("file");
+ writer.OnStringScalar(position.File);
+ }
+ }
+ writer.OnEndMap();
+ };
+
+ auto fn = [&] (const NYql::TIssue& issue, ui16 /*level*/) {
+ writer.OnListItem();
+ writer.OnBeginMap();
+ {
+ writer.OnKeyedItem("code");
+
+ NYql::TIssueCode code = IssueToErrorCodesShift + issue.GetCode();
+ writer.OnInt64Scalar(code);
+
+ writer.OnKeyedItem("message");
+ writer.OnStringScalar(issue.GetMessage());
+
+ writer.OnKeyedItem("attributes");
+ writer.OnBeginMap();
+ {
+ if (issue.Range().Position) {
+ writer.OnKeyedItem("start_position");
+ serializePosition(issue.Range().Position);
+ }
+
+ if (issue.Range().EndPosition) {
+ writer.OnKeyedItem("end_position");
+ serializePosition(issue.Range().EndPosition);
+ }
+
+ writer.OnKeyedItem("yql_status");
+ writer.OnStringScalar(NYql::IssueCodeToString<NYql::TIssuesIds>(issue.GetCode()));
+
+ writer.OnKeyedItem("severity");
+ writer.OnStringScalar(SeverityToString(issue.GetSeverity()));
+ }
+ writer.OnEndMap();
+
+ writer.OnKeyedItem("inner_errors");
+ writer.OnBeginList();
+ }
+ };
+
+ auto afterChildrenFn = [&] (const NYql::TIssue& /*issue*/, ui16 /*level*/) {
+ writer.OnEndList();
+ writer.OnEndMap();
+ };
+
+ writer.OnBeginMap();
+ writer.OnKeyedItem("message");
+ writer.OnStringScalar("There are some issues");
+ writer.OnKeyedItem("code");
+ writer.OnInt64Scalar(1);
+ writer.OnKeyedItem("inner_errors");
+ writer.OnBeginList();
+ for (const auto& issue : issues) {
+ WalkThroughIssues(issue, /*leafOnly*/ false, fn, afterChildrenFn);
+ }
+ writer.OnEndList();
+ writer.OnKeyedItem("attributes");
+ writer.OnBeginMap();
+ writer.OnEndMap();
+ writer.OnEndMap();
+ return yson.Str();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPLugin
diff --git a/yt/yql/plugin/native/error_helpers.h b/yt/yql/plugin/native/error_helpers.h
new file mode 100644
index 0000000000..1905502aa8
--- /dev/null
+++ b/yt/yql/plugin/native/error_helpers.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+#include <ydb/library/yql/public/issue/yql_issue.h>
+
+namespace NYT::NYqlPlugin {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString IssuesToYtErrorYson(const NYql::TIssues& issues);
+
+TString ExceptionToYtErrorYson(const std::exception& exception);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPlugin
diff --git a/yt/yql/plugin/native/plugin.cpp b/yt/yql/plugin/native/plugin.cpp
new file mode 100644
index 0000000000..086f63a2f9
--- /dev/null
+++ b/yt/yql/plugin/native/plugin.cpp
@@ -0,0 +1,293 @@
+#include "plugin.h"
+
+#include "error_helpers.h"
+
+#include <ydb/library/yql/providers/yt/lib/log/yt_logger.h>
+#include <ydb/library/yql/providers/yt/lib/yt_download/yt_download.h>
+#include <ydb/library/yql/providers/yt/gateway/native/yql_yt_native.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+
+#include <ydb/library/yql/core/url_preprocessing/url_preprocessing.h>
+
+#include <ydb/library/yql/providers/common/udf_resolve/yql_simple_udf_resolver.h>
+#include "ydb/library/yql/providers/common/proto/gateways_config.pb.h"
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+#include <ydb/library/yql/core/facade/yql_facade.h>
+#include <ydb/library/yql/core/file_storage/file_storage.h>
+#include "ydb/library/yql/core/file_storage/proto/file_storage.pb.h"
+#include <ydb/library/yql/core/services/mounts/yql_mounts.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/utils/backtrace/backtrace.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/logging/logger.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/yson/parser.h>
+#include <library/cpp/yson/writer.h>
+
+#include <library/cpp/resource/resource.h>
+#include <library/cpp/digest/md5/md5.h>
+
+#include <util/folder/path.h>
+#include <util/stream/file.h>
+#include <util/string/builder.h>
+#include <util/system/fs.h>
+#include <util/system/user.h>
+
+namespace NYT::NYqlPlugin {
+namespace NNative {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYqlPlugin
+ : public IYqlPlugin
+{
+public:
+ TYqlPlugin(TYqlPluginOptions& options)
+ {
+ try {
+ NYql::NLog::InitLogger(std::move(options.LogBackend));
+
+ auto& logger = NYql::NLog::YqlLogger();
+
+ logger.SetDefaultPriority(ELogPriority::TLOG_DEBUG);
+ for (int i = 0; i < NYql::NLog::EComponentHelpers::ToInt(NYql::NLog::EComponent::MaxValue); ++i) {
+ logger.SetComponentLevel((NYql::NLog::EComponent) i, NYql::NLog::ELevel::DEBUG);
+ }
+
+ NYql::SetYtLoggerGlobalBackend(NYT::ILogger::ELevel::DEBUG);
+ if (NYT::TConfig::Get()->Prefix.empty()) {
+ NYT::TConfig::Get()->Prefix = "//";
+ }
+
+ auto yqlCoreFlags = GatewaysConfig_.GetYqlCore()
+ .GetFlags();
+
+ auto ytConfig = GatewaysConfig_.MutableYt();
+ if (!ytConfig->HasExecuteUdfLocallyIfPossible()) {
+ ytConfig->SetExecuteUdfLocallyIfPossible(true);
+ }
+
+ ytConfig->SetYtLogLevel(NYql::EYtLogLevel::YL_DEBUG);
+ ytConfig->SetMrJobBin(options.MRJobBinary);
+ ytConfig->SetMrJobBinMd5(MD5::File(options.MRJobBinary));
+
+ ytConfig->ClearMrJobUdfsDir();
+
+ for (const auto& [cluster, address]: options.Clusters) {
+ auto item = ytConfig->AddClusterMapping();
+ item->SetName(cluster);
+ item->SetCluster(address);
+ if (cluster == options.DefaultCluster) {
+ item->SetDefault(true);
+ }
+
+ Clusters_.insert({item->GetName(), TString(NYql::YtProviderName)});
+ }
+ DefaultCluster_ = options.DefaultCluster;
+
+ NYql::TFileStorageConfig fileStorageConfig;
+ fileStorageConfig.SetMaxSizeMb(1 << 14);
+ FileStorage_ = WithAsync(CreateFileStorage(fileStorageConfig, {MakeYtDownloader(fileStorageConfig)}));
+
+ FuncRegistry_ = NKikimr::NMiniKQL::CreateFunctionRegistry(
+ NKikimr::NMiniKQL::CreateBuiltinRegistry())->Clone();
+
+ const NKikimr::NMiniKQL::TUdfModuleRemappings emptyRemappings;
+
+ FuncRegistry_->SetBackTraceCallback(&NYql::NBacktrace::KikimrBackTrace);
+
+ NKikimr::NMiniKQL::TUdfModulePathsMap systemModules;
+
+ TVector<TString> udfPaths;
+ NKikimr::NMiniKQL::FindUdfsInDir(options.UdfDirectory, &udfPaths);
+ for (const auto& path: udfPaths) {
+ // Skip YQL plugin shared library itself, it is not a UDF.
+ if (path.EndsWith("libyqlplugin.so")) {
+ continue;
+ }
+ FuncRegistry_->LoadUdfs(path, emptyRemappings, 0);
+ }
+
+ for (auto& m: FuncRegistry_->GetAllModuleNames()) {
+ TMaybe<TString> path = FuncRegistry_->FindUdfPath(m);
+ if (!path) {
+ YQL_LOG(FATAL) << "Unable to detect UDF path for module " << m;
+ exit(1);
+ }
+ systemModules.emplace(m, *path);
+ }
+
+ FuncRegistry_->SetSystemModulePaths(systemModules);
+
+ NYql::TUserDataTable userDataTable = GetYqlModuleResolver(ExprContext_, ModuleResolver_, {}, Clusters_, {});
+
+ if (!userDataTable) {
+ TStringStream err;
+ ExprContext_.IssueManager
+ .GetIssues()
+ .PrintTo(err);
+ YQL_LOG(FATAL) << "Failed to compile modules:\n"
+ << err.Str();
+ exit(1);
+ }
+
+ OperationAttributes_ = options.OperationAttributes;
+
+ TVector<NYql::TDataProviderInitializer> dataProvidersInit;
+
+ NYql::TYtNativeServices ytServices;
+ ytServices.FunctionRegistry = FuncRegistry_.Get();
+ ytServices.FileStorage = FileStorage_;
+ ytServices.Config = std::make_shared<NYql::TYtGatewayConfig>(*ytConfig);
+ auto ytNativeGateway = CreateYtNativeGateway(ytServices);
+ dataProvidersInit.push_back(GetYtNativeDataProviderInitializer(ytNativeGateway));
+
+ ProgramFactory_ = std::make_unique<NYql::TProgramFactory>(
+ false, FuncRegistry_.Get(), ExprContext_.NextUniqueId, dataProvidersInit, "embedded");
+ YTTokenPath_ = options.YTTokenPath;
+ ProgramFactory_->AddUserDataTable(userDataTable);
+ ProgramFactory_->SetModules(ModuleResolver_);
+ ProgramFactory_->SetUdfResolver(NYql::NCommon::CreateSimpleUdfResolver(FuncRegistry_.Get(), FileStorage_));
+ ProgramFactory_->SetGatewaysConfig(&GatewaysConfig_);
+ ProgramFactory_->SetFileStorage(FileStorage_);
+ ProgramFactory_->SetUrlPreprocessing(MakeIntrusive<NYql::TUrlPreprocessing>(GatewaysConfig_));
+ } catch (const std::exception& ex) {
+ YQL_LOG(FATAL) << "Unexpected exception while initializing YQL plugin: " << ex.what();
+ exit(1);
+ }
+ YQL_LOG(INFO) << "YQL plugin initialized";
+ }
+
+ TQueryResult GuardedRun(TString impersonationUser, TString queryText, TYsonString settings)
+ {
+ auto credentials = MakeIntrusive<NYql::TCredentials>();
+ if (YTTokenPath_) {
+ TFsPath path(YTTokenPath_);
+ auto token = TIFStream(path).ReadAll();
+
+ credentials->AddCredential("default_yt", NYql::TCredential("yt", "", token));
+ }
+
+ credentials->AddCredential("impersonation_user_yt", NYql::TCredential("yt", "", impersonationUser));
+ ProgramFactory_->SetCredentials(credentials);
+
+ auto program = ProgramFactory_->Create("-memory-", queryText);
+ program->SetOperationAttrsYson(PatchQueryAttributes(OperationAttributes_, settings));
+
+ NSQLTranslation::TTranslationSettings sqlSettings;
+ sqlSettings.ClusterMapping = Clusters_;
+ sqlSettings.ModuleMapping = Modules_;
+ if (DefaultCluster_) {
+ sqlSettings.DefaultCluster = *DefaultCluster_;
+ }
+ sqlSettings.SyntaxVersion = 1;
+ sqlSettings.V0Behavior = NSQLTranslation::EV0Behavior::Disable;
+
+ if (!program->ParseSql(sqlSettings)) {
+ return TQueryResult{
+ .YsonError = IssuesToYtErrorYson(program->Issues()),
+ };
+ }
+
+ if (!program->Compile(GetUsername())) {
+ return TQueryResult{
+ .YsonError = IssuesToYtErrorYson(program->Issues()),
+ };
+ }
+
+ NYql::TProgram::TStatus status = NYql::TProgram::TStatus::Error;
+ status = program->Run(GetUsername(), nullptr, nullptr, nullptr);
+
+ if (status == NYql::TProgram::TStatus::Error) {
+ return TQueryResult{
+ .YsonError = IssuesToYtErrorYson(program->Issues()),
+ };
+ }
+
+ TStringStream result;
+ if (program->HasResults()) {
+ ::NYson::TYsonWriter yson(&result, EYsonFormat::Binary);
+ yson.OnBeginList();
+ for (const auto& result: program->Results()) {
+ yson.OnListItem();
+ yson.OnRaw(result);
+ }
+ yson.OnEndList();
+ }
+
+ auto maybeToOptional = [] (const TMaybe<TString>& maybeStr) -> std::optional<TString> {
+ if (!maybeStr) {
+ return std::nullopt;
+ }
+ return *maybeStr;
+ };
+
+ return {
+ .YsonResult = result.Empty() ? std::nullopt : std::make_optional(result.Str()),
+ .Plan = maybeToOptional(program->GetQueryPlan()),
+ .Statistics = maybeToOptional(program->GetStatistics()),
+ .TaskInfo = maybeToOptional(program->GetTasksInfo()),
+ };
+ }
+
+ TQueryResult Run(TString impersonationUser, TString queryText, TYsonString settings) noexcept override
+ {
+ try {
+ return GuardedRun(impersonationUser, queryText, settings);
+ } catch (const std::exception& ex) {
+ return TQueryResult{
+ .YsonError = ExceptionToYtErrorYson(ex),
+ };
+ }
+ }
+
+private:
+ NYql::TFileStoragePtr FileStorage_;
+ NYql::TExprContext ExprContext_;
+ ::TIntrusivePtr<NKikimr::NMiniKQL::IMutableFunctionRegistry> FuncRegistry_;
+ NYql::IModuleResolver::TPtr ModuleResolver_;
+ NYql::TGatewaysConfig GatewaysConfig_;
+ std::unique_ptr<NYql::TProgramFactory> ProgramFactory_;
+ TString YTTokenPath_;
+ THashMap<TString, TString> Clusters_;
+ std::optional<TString> DefaultCluster_;
+ THashMap<TString, TString> Modules_;
+ THashSet<TString> Libraries_;
+ TYsonString OperationAttributes_;
+
+ TString PatchQueryAttributes(TYsonString configAttributes, TYsonString querySettings)
+ {
+ NYT::TNode querySettingsMap = NodeFromYsonString(querySettings.ToString());
+ NYT::TNode resultAttributesMap = NodeFromYsonString(configAttributes.ToString());
+
+ for (const auto& item: querySettingsMap.AsMap()) {
+ resultAttributesMap[item.first] = item.second;
+ }
+
+ return NodeToYsonString(resultAttributesMap);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NNative
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IYqlPlugin> CreateYqlPlugin(TYqlPluginOptions& options) noexcept
+{
+ return std::make_unique<NNative::TYqlPlugin>(options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPlugin
diff --git a/yt/yql/plugin/native/plugin.h b/yt/yql/plugin/native/plugin.h
new file mode 100644
index 0000000000..53bb7119ce
--- /dev/null
+++ b/yt/yql/plugin/native/plugin.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <yt/yql/plugin/plugin.h>
+
+
+namespace NYT::NYqlPlugin {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IYqlPlugin> CreateYqlPlugin(TYqlPluginOptions& options) noexcept;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPlugin
diff --git a/yt/yql/plugin/native/ya.make b/yt/yql/plugin/native/ya.make
new file mode 100644
index 0000000000..36d562a4d2
--- /dev/null
+++ b/yt/yql/plugin/native/ya.make
@@ -0,0 +1,44 @@
+LIBRARY()
+
+SRCS(
+ GLOBAL plugin.cpp
+ error_helpers.cpp
+)
+
+PEERDIR(
+ contrib/libs/protobuf
+ library/cpp/resource
+ library/cpp/yson
+ library/cpp/yson/node
+ yt/cpp/mapreduce/client
+ yt/cpp/mapreduce/common
+ ydb/library/yql/ast
+ ydb/library/yql/sql/pg
+ ydb/library/yql/parser/pg_wrapper
+ ydb/library/yql/core/facade
+ ydb/library/yql/core/file_storage
+ ydb/library/yql/core/file_storage/proto
+ ydb/library/yql/core/file_storage/http_download
+ ydb/library/yql/core/services/mounts
+ ydb/library/yql/core/user_data
+ ydb/library/yql/minikql
+ ydb/library/yql/protos
+ ydb/library/yql/public/udf/service/exception_policy
+ ydb/library/yql/utils/backtrace
+ ydb/library/yql/utils/log
+ ydb/library/yql/providers/common/proto
+ ydb/library/yql/providers/common/udf_resolve
+ ydb/library/yql/providers/solomon/gateway
+ ydb/library/yql/providers/solomon/provider
+ ydb/library/yql/core/url_preprocessing
+ ydb/library/yql/providers/yt/gateway/native
+ ydb/library/yql/providers/yt/lib/log
+ ydb/library/yql/providers/yt/lib/yt_download
+ ydb/library/yql/providers/yt/provider
+
+ yt/yql/plugin
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/yt/yql/plugin/plugin.cpp b/yt/yql/plugin/plugin.cpp
new file mode 100644
index 0000000000..5ebfffb796
--- /dev/null
+++ b/yt/yql/plugin/plugin.cpp
@@ -0,0 +1,18 @@
+#include "plugin.h"
+
+#include <iostream>
+
+namespace NYT::NYqlPlugin {
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_WEAK std::unique_ptr<IYqlPlugin> CreateYqlPlugin(TYqlPluginOptions& /*options*/) noexcept
+{
+ std::cerr << "No YQL plugin implementation is available; link against either "
+ << "yt/yql/plugin/native or yt/yql/plugin/dynamic" << std::endl;
+ exit(1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPlugin
diff --git a/yt/yql/plugin/plugin.h b/yt/yql/plugin/plugin.h
new file mode 100644
index 0000000000..46a08ba5e7
--- /dev/null
+++ b/yt/yql/plugin/plugin.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+
+#include <library/cpp/logger/log.h>
+
+#include <library/cpp/yt/yson_string/string.h>
+
+#include <optional>
+
+namespace NYT::NYqlPlugin {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYqlPluginOptions
+{
+public:
+ TString MRJobBinary = "./mrjob";
+ TString UdfDirectory;
+
+ //! Mapping cluster name -> proxy address.
+ THashMap<TString, TString> Clusters;
+ std::optional<TString> DefaultCluster;
+
+ TYsonString OperationAttributes;
+
+ TString YTTokenPath;
+
+ THolder<TLogBackend> LogBackend;
+
+ std::optional<TString> YqlPluginSharedLibrary;
+};
+
+struct TQueryResult
+{
+ std::optional<TString> YsonResult;
+ std::optional<TString> Plan;
+ std::optional<TString> Statistics;
+ std::optional<TString> TaskInfo;
+
+ //! YSON representation of a YT error.
+ std::optional<TString> YsonError;
+};
+
+//! This interface encapsulates YT <-> YQL integration.
+//! There are two major implementation: one of them is based
+//! on YQL code and another wraps the pure C bridge interface, which
+//! is implemented by a dynamic library.
+struct IYqlPlugin
+{
+ virtual TQueryResult Run(TString impersonationUser, TString queryText, TYsonString settings) noexcept = 0;
+
+ virtual ~IYqlPlugin() = default;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_WEAK std::unique_ptr<IYqlPlugin> CreateYqlPlugin(TYqlPluginOptions& options) noexcept;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYqlPlugin
diff --git a/yt/yql/plugin/ya.make b/yt/yql/plugin/ya.make
new file mode 100644
index 0000000000..7046752106
--- /dev/null
+++ b/yt/yql/plugin/ya.make
@@ -0,0 +1,19 @@
+LIBRARY()
+
+SRCS(
+ plugin.cpp
+)
+
+END()
+
+RECURSE(
+ bridge
+)
+
+IF (NOT OPENSOURCE)
+ # We do not bring YQL with us into open source.
+ RECURSE(
+ dynamic
+ native
+ )
+ENDIF()
diff --git a/yt/yt/CMakeLists.txt b/yt/yt/CMakeLists.txt
new file mode 100644
index 0000000000..b0414f0741
--- /dev/null
+++ b/yt/yt/CMakeLists.txt
@@ -0,0 +1,11 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(build)
+add_subdirectory(core)
+add_subdirectory(library)
diff --git a/yt/yt/build/CMakeLists.darwin-x86_64.txt b/yt/yt/build/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..da9bc5854d
--- /dev/null
+++ b/yt/yt/build/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,96 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+set(
+ HAVE_CXXABI_H
+ 0
+)
+set(
+ HAVE_DLFCN_H
+ 1
+)
+set(
+ HAVE_EXECINFO_H
+ 1
+)
+set(
+ HAVE_LIBUNWIND_H
+ 0
+)
+set(
+ HAVE_PTHREAD_H
+ 1
+)
+set(
+ HAVE_SYS_TYPES_H
+ 1
+)
+set(
+ HAVE_SYS_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UNISTD_H
+ 1
+)
+set(
+ HAVE_UNWIND_H
+ 0
+)
+set(
+ PC_FROM_UCONTEXT
+ uc_mcontext.gregs[REG_RIP]
+)
+set(
+ YT_VERSION_BRANCH
+ local
+)
+set(
+ YT_VERSION_MAJOR
+ 23
+)
+set(
+ YT_VERSION_MINOR
+ 2
+)
+set(
+ YT_VERSION_PATCH
+ 0
+)
+set(
+ YT_VERSION_TYPE
+ os
+)
+
+add_library(yt-yt-build)
+target_compile_options(yt-yt-build PRIVATE
+ -Wdeprecated-this-capture
+)
+target_include_directories(yt-yt-build PUBLIC
+ ${CMAKE_BINARY_DIR}/yt/yt/build
+)
+target_link_libraries(yt-yt-build PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yt-build PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/ya_version.cpp
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/config.h.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/config.h
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/build.cpp.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
diff --git a/yt/yt/build/CMakeLists.linux-aarch64.txt b/yt/yt/build/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..7104ebd952
--- /dev/null
+++ b/yt/yt/build/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,97 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+set(
+ HAVE_CXXABI_H
+ 0
+)
+set(
+ HAVE_DLFCN_H
+ 1
+)
+set(
+ HAVE_EXECINFO_H
+ 1
+)
+set(
+ HAVE_LIBUNWIND_H
+ 0
+)
+set(
+ HAVE_PTHREAD_H
+ 1
+)
+set(
+ HAVE_SYS_TYPES_H
+ 1
+)
+set(
+ HAVE_SYS_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UNISTD_H
+ 1
+)
+set(
+ HAVE_UNWIND_H
+ 0
+)
+set(
+ PC_FROM_UCONTEXT
+ uc_mcontext.gregs[REG_RIP]
+)
+set(
+ YT_VERSION_BRANCH
+ local
+)
+set(
+ YT_VERSION_MAJOR
+ 23
+)
+set(
+ YT_VERSION_MINOR
+ 2
+)
+set(
+ YT_VERSION_PATCH
+ 0
+)
+set(
+ YT_VERSION_TYPE
+ os
+)
+
+add_library(yt-yt-build)
+target_compile_options(yt-yt-build PRIVATE
+ -Wdeprecated-this-capture
+)
+target_include_directories(yt-yt-build PUBLIC
+ ${CMAKE_BINARY_DIR}/yt/yt/build
+)
+target_link_libraries(yt-yt-build PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yt-build PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/ya_version.cpp
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/config.h.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/config.h
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/build.cpp.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
diff --git a/yt/yt/build/CMakeLists.linux-x86_64.txt b/yt/yt/build/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..7104ebd952
--- /dev/null
+++ b/yt/yt/build/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,97 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+set(
+ HAVE_CXXABI_H
+ 0
+)
+set(
+ HAVE_DLFCN_H
+ 1
+)
+set(
+ HAVE_EXECINFO_H
+ 1
+)
+set(
+ HAVE_LIBUNWIND_H
+ 0
+)
+set(
+ HAVE_PTHREAD_H
+ 1
+)
+set(
+ HAVE_SYS_TYPES_H
+ 1
+)
+set(
+ HAVE_SYS_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UNISTD_H
+ 1
+)
+set(
+ HAVE_UNWIND_H
+ 0
+)
+set(
+ PC_FROM_UCONTEXT
+ uc_mcontext.gregs[REG_RIP]
+)
+set(
+ YT_VERSION_BRANCH
+ local
+)
+set(
+ YT_VERSION_MAJOR
+ 23
+)
+set(
+ YT_VERSION_MINOR
+ 2
+)
+set(
+ YT_VERSION_PATCH
+ 0
+)
+set(
+ YT_VERSION_TYPE
+ os
+)
+
+add_library(yt-yt-build)
+target_compile_options(yt-yt-build PRIVATE
+ -Wdeprecated-this-capture
+)
+target_include_directories(yt-yt-build PUBLIC
+ ${CMAKE_BINARY_DIR}/yt/yt/build
+)
+target_link_libraries(yt-yt-build PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yt-build PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/ya_version.cpp
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/config.h.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/config.h
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/build.cpp.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
diff --git a/yt/yt/build/CMakeLists.txt b/yt/yt/build/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/build/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/build/CMakeLists.windows-x86_64.txt b/yt/yt/build/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..4bcabf1d85
--- /dev/null
+++ b/yt/yt/build/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,93 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+set(
+ HAVE_CXXABI_H
+ 0
+)
+set(
+ HAVE_DLFCN_H
+ 1
+)
+set(
+ HAVE_EXECINFO_H
+ 1
+)
+set(
+ HAVE_LIBUNWIND_H
+ 0
+)
+set(
+ HAVE_PTHREAD_H
+ 1
+)
+set(
+ HAVE_SYS_TYPES_H
+ 1
+)
+set(
+ HAVE_SYS_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UCONTEXT_H
+ 1
+)
+set(
+ HAVE_UNISTD_H
+ 1
+)
+set(
+ HAVE_UNWIND_H
+ 0
+)
+set(
+ PC_FROM_UCONTEXT
+ uc_mcontext.gregs[REG_RIP]
+)
+set(
+ YT_VERSION_BRANCH
+ local
+)
+set(
+ YT_VERSION_MAJOR
+ 23
+)
+set(
+ YT_VERSION_MINOR
+ 2
+)
+set(
+ YT_VERSION_PATCH
+ 0
+)
+set(
+ YT_VERSION_TYPE
+ os
+)
+
+add_library(yt-yt-build)
+target_include_directories(yt-yt-build PUBLIC
+ ${CMAKE_BINARY_DIR}/yt/yt/build
+)
+target_link_libraries(yt-yt-build PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+)
+target_sources(yt-yt-build PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/ya_version.cpp
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/config.h.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/config.h
+)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/yt/yt/build/build.cpp.in
+ ${CMAKE_BINARY_DIR}/yt/yt/build/build.cpp
+)
diff --git a/yt/yt/build/build.cpp.in b/yt/yt/build/build.cpp.in
new file mode 100644
index 0000000000..0f8f6a911e
--- /dev/null
+++ b/yt/yt/build/build.cpp.in
@@ -0,0 +1,59 @@
+#include <yt/yt/build/build.h>
+
+#include <yt/yt/build/ya_version.h>
+
+#include <util/system/compiler.h>
+
+#include <util/generic/strbuf.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_WEAK int GetVersionMajor()
+{
+ return @YT_VERSION_MAJOR@;
+}
+
+Y_WEAK int GetVersionMinor()
+{
+ return @YT_VERSION_MINOR@;
+}
+
+Y_WEAK int GetVersionPatch()
+{
+ return @YT_VERSION_PATCH@;
+}
+
+Y_WEAK const char* GetBranch()
+{
+ return "@YT_VERSION_BRANCH@";
+}
+
+Y_WEAK const char* GetVersionType()
+{
+ return "@YT_VERSION_TYPE@";
+}
+
+Y_WEAK const char* GetVersion()
+{
+ static auto version = CreateYTVersion(@YT_VERSION_MAJOR@, @YT_VERSION_MINOR@, @YT_VERSION_PATCH@, "@YT_VERSION_BRANCH@");
+ return version.c_str();
+}
+
+const char* GetBuildHost()
+{
+ static auto buildHost = GetYaHostName();
+ return buildHost.data();
+}
+
+const char* GetBuildTime()
+{
+ static auto buildDate = GetYaBuildDate();
+ return buildDate.data();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/build/build.h b/yt/yt/build/build.h
new file mode 100644
index 0000000000..6917a31b9e
--- /dev/null
+++ b/yt/yt/build/build.h
@@ -0,0 +1,19 @@
+#pragma once
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+int GetVersionMajor();
+int GetVersionMinor();
+int GetVersionPatch();
+const char* GetBranch();
+const char* GetVersion();
+const char* GetVersionType();
+const char* GetBuildHost();
+const char* GetBuildTime();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/build/config.h.in b/yt/yt/build/config.h.in
new file mode 100644
index 0000000000..bc4821df51
--- /dev/null
+++ b/yt/yt/build/config.h.in
@@ -0,0 +1,49 @@
+#pragma once
+
+#ifndef _win_
+
+#ifndef HAVE_CXXABI_H
+ #cmakedefine HAVE_CXXABI_H
+#endif
+
+#ifndef HAVE_DLFCN_H
+ #cmakedefine HAVE_DLFCN_H
+#endif
+
+#ifndef HAVE_EXECINFO_H
+ #cmakedefine HAVE_EXECINFO_H
+#endif
+
+#ifndef HAVE_LIBUNWIND_H
+ #cmakedefine HAVE_LIBUNWIND_H
+#endif
+
+#ifndef HAVE_PTHREAD_H
+ #cmakedefine HAVE_PTHREAD_H
+#endif
+
+#ifndef HAVE_SYS_TYPES_H
+ #cmakedefine HAVE_SYS_TYPES_H
+#endif
+
+#ifndef HAVE_SYS_UCONTEXT_H
+ #cmakedefine HAVE_SYS_UCONTEXT_H
+#endif
+
+#ifndef HAVE_UCONTEXT_H
+ #cmakedefine HAVE_UCONTEXT_H
+#endif
+
+#ifndef HAVE_UNWIND_H
+ #cmakedefine HAVE_UNWIND_H
+#endif
+
+#ifndef HAVE_UNISTD_H
+ #cmakedefine HAVE_UNISTD_H
+#endif
+
+#endif
+
+#ifdef __x86_64__
+ #cmakedefine PC_FROM_UCONTEXT @PC_FROM_UCONTEXT@
+#endif
diff --git a/yt/yt/build/ya.make b/yt/yt/build/ya.make
new file mode 100644
index 0000000000..45923fc7ac
--- /dev/null
+++ b/yt/yt/build/ya.make
@@ -0,0 +1,45 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SET(YT_VERSION_MAJOR 23)
+SET(YT_VERSION_MINOR 2)
+
+DEFAULT(YT_VERSION_PATCH 0)
+DEFAULT(YT_VERSION_BRANCH "local")
+
+# We define this variable in order to mute ya's warnings.
+# Actually version will be computed from SVNVERSION.
+SET(YT_VERSION "version-unknown")
+
+SET(YT_BUILD_HOST "")
+SET(YT_BUILD_MACHINE "")
+SET(YT_BUILD_TIME "")
+
+SET(HAVE_CXXABI_H 0)
+SET(HAVE_DLFCN_H 1)
+SET(HAVE_EXECINFO_H 1)
+SET(HAVE_LIBUNWIND_H 0)
+SET(HAVE_PTHREAD_H 1)
+SET(HAVE_SYS_TYPES_H 1)
+SET(HAVE_SYS_UCONTEXT_H 1)
+SET(HAVE_UCONTEXT_H 1)
+SET(HAVE_UNISTD_H 1)
+SET(HAVE_UNWIND_H 0)
+SET(PC_FROM_UCONTEXT "uc_mcontext.gregs[REG_RIP]")
+
+IF (NOT OPENSOURCE)
+ SET(YT_VERSION_TYPE "ya")
+ELSE()
+ SET(YT_VERSION_TYPE "os")
+ENDIF()
+
+SRCS(
+ config.h.in
+ build.cpp.in
+ build.h
+
+ ya_version.cpp
+)
+
+END()
diff --git a/yt/yt/build/ya_version.cpp b/yt/yt/build/ya_version.cpp
new file mode 100644
index 0000000000..a92f5c131f
--- /dev/null
+++ b/yt/yt/build/ya_version.cpp
@@ -0,0 +1,158 @@
+#include "ya_version.h"
+
+#include "build.h"
+
+#include <build/scripts/c_templates/svnversion.h>
+
+#include <util/stream/format.h>
+#include <util/stream/str.h>
+
+#include <util/system/compiler.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetCommitHash()
+{
+ TString commit = GetProgramHash();
+ if (commit.empty()) {
+ commit = GetProgramCommitId();
+ }
+
+ return commit;
+}
+
+TString TruncateCommitHash(TString commit)
+{
+ constexpr int CommitHashPrefixLength = 16;
+
+ // When we use `ya make --dist` distbuild makes mess instead of svn revision:
+ // BUILD_USER == "Unknown user"
+ // ARCADIA_SOURCE_REVISION = "-1"
+ // When Hermes looks at such horrors it goes crazy.
+ // Here are some hacks to help Hermes keep its sanity.
+ if (commit == "-1") {
+ commit = TString(CommitHashPrefixLength, '0');
+ } else {
+ commit = commit.substr(0, CommitHashPrefixLength);
+ }
+ return commit;
+}
+
+void OutputCreateBranchCommitVersion(TStringBuf branch, TStringStream& out)
+{
+ out << branch << "-" << GetVersionType();
+
+#if !defined(NDEBUG)
+ out << "debug";
+#endif
+
+#if defined(_asan_enabled_)
+ out << "-asan";
+#endif
+
+ TString commit;
+ int svnRevision = GetProgramSvnRevision();
+ if (svnRevision <= 0) {
+ commit = GetCommitHash();
+ commit = TruncateCommitHash(commit);
+ } else {
+ commit = "r" + ToString(svnRevision);
+ }
+
+ TString buildUser = GetProgramBuildUser();
+ if (buildUser == "Unknown user") {
+ buildUser = "distbuild";
+ }
+
+ out << "~" << commit;
+ if (buildUser != "teamcity") {
+ out << "+" << buildUser;
+ }
+}
+
+TString CreateBranchCommitVersion(TStringBuf branch)
+{
+ TStringStream out;
+ OutputCreateBranchCommitVersion(branch, out);
+ return out.Str();
+}
+
+TString CreateYTVersion(int major, int minor, int patch, TStringBuf branch)
+{
+ TStringStream out;
+ out << major << "." << minor << "." << patch << "-";
+ OutputCreateBranchCommitVersion(branch, out);
+ return out.Str();
+}
+
+TString GetYaHostName()
+{
+ return GetProgramBuildHost();
+}
+
+TString GetYaBuildDate()
+{
+ return GetProgramBuildDate();
+}
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString BuildRpcUserAgent() noexcept
+{
+ // Fill user agent from build information.
+ // For trunk:
+ // - yt-cpp/r<revision>
+ // For YT release branch.
+ // - yt-cpp/<YT version string>
+ // For arbitrary branch different from trunk:
+ // - yt-cpp/<branch>~<commit>
+ // For local build from detached head or arc sync'ed state:
+ // - yt-cpp/local~<commit>
+
+ TString branch(GetBranch());
+ TStringStream out;
+
+ out << "yt-cpp/";
+
+ if (branch == "trunk") {
+ int svnRevision = GetProgramSvnRevision();
+ out << "trunk~r" << svnRevision;
+ } else if (branch.StartsWith("releases/yt")) {
+ // Simply re-use YT version string. It looks like the following:
+ // 20.3.7547269-stable-ya~bb57c034bfb47caa.
+ TString ytVersion(GetVersion());
+ out << ytVersion;
+ } else {
+ auto commit = GetCommitHash();
+ auto truncatedCommit = TruncateCommitHash(commit);
+
+ // In detached head arc state branch seems to coincide with commit hash.
+ // Let's use that in order to distinguish detached head from regular branch state.
+ if (branch == commit) {
+ branch = "local";
+ }
+
+ out << branch << "~" << truncatedCommit;
+ }
+
+ return out.Str();
+}
+
+const TString CachedUserAgent = BuildRpcUserAgent();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+const TString& GetRpcUserAgent()
+{
+ return CachedUserAgent;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/build/ya_version.h b/yt/yt/build/ya_version.h
new file mode 100644
index 0000000000..2cb9a48853
--- /dev/null
+++ b/yt/yt/build/ya_version.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString CreateBranchCommitVersion(TStringBuf branch);
+TString CreateYTVersion(int major, int minor, int patch, TStringBuf branch);
+TString GetYaHostName();
+TString GetYaBuildDate();
+
+const TString& GetRpcUserAgent();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/CMakeLists.darwin-x86_64.txt b/yt/yt/core/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..7aa581296e
--- /dev/null
+++ b/yt/yt/core/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,366 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+find_package(ZLIB REQUIRED)
+find_package(c-ares REQUIRED)
+add_subdirectory(crypto)
+add_subdirectory(http)
+add_subdirectory(https)
+add_subdirectory(misc)
+
+add_library(yt-yt-core)
+target_compile_options(yt-yt-core PRIVATE
+ -mpclmul
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-yt-core PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-libunwind
+)
+target_sources(yt-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/cancelable_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/current_invoker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/future.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_util.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/packet.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/ssl_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/brotli.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/bzip2.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/codec.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lz.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lzma.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/snappy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zlib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zstd.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_barrier.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_semaphore.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_rw_lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/coroutine.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/delayed_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/execution_stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fls.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/notify_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_alarm.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_yielder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/pollable_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/profiling_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/propagating_storage.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/quantized_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/suspendable_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/system_invokers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_affinity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/throughput_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/lease_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/fluent_log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/logger_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/serializable_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_writer_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/file_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/random_access_gzip.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/zstd_compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/arithmetic_formula.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bitmap.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packed_unsigned_vector.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/blob_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bloom_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/checksum.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/coro_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/crash_handler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/digest.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/dnf.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error_code.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ema_counter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/fs.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hazard_ptr.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hedging_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/histogram.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/historic_usage_aggregator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hr_timer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/id_generator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/linear_probe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_reference_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_usage_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/relaxed_mpsc_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/parser_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pattern_formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/phoenix.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pool_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/proc.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/protobuf_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_profiler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/shutdown.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/signal_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/slab_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/cache_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/utf8_decoder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/zerocopy_output_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/dialer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/listener.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/local_address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/socket.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/ares_dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/profiling/timing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authentication_identity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authenticator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/balancing_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/caching_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/channel_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dynamic_channel_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/hedging_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/null_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/per_user_request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/protocol_version.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/response_keeper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/retrying_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/roaming_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/serialized_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/server_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/static_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/throttling_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/viable_peer_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/service_discovery/service_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/spin_wait_slow_path_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_tags.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/utilex/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/forwarding_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/lexer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/null_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_unknown_fields.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser_deserialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/syntax_checker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_merger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/ypath_designated_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/depth_limiting_yson_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attributes_stripper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/convert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_attribute_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_node_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/exception_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/interned_attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/permission.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/request_complexity_limiter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/static_service_dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/system_attribute_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_builder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_visitor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/virtual.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/service_combiner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_serializable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_callbacks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/bindings.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/statistics_producer.cpp
+)
+
+add_global_library_for(yt-yt-core.global yt-yt-core)
+target_compile_options(yt-yt-core.global PRIVATE
+ -mpclmul
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-yt-core.global PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-libunwind
+)
+target_sources(yt-yt-core.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/assert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/guid.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_tracked.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_hooks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/trace_context.cpp
+)
diff --git a/yt/yt/core/CMakeLists.linux-aarch64.txt b/yt/yt/core/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..611dbe9b4c
--- /dev/null
+++ b/yt/yt/core/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,366 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+find_package(ZLIB REQUIRED)
+find_package(c-ares REQUIRED)
+add_subdirectory(crypto)
+add_subdirectory(http)
+add_subdirectory(https)
+add_subdirectory(misc)
+
+add_library(yt-yt-core)
+target_compile_options(yt-yt-core PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-yt-core PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-libunwind
+)
+target_sources(yt-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/cancelable_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/current_invoker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/future.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_util.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/packet.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/ssl_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/brotli.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/bzip2.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/codec.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lz.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lzma.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/snappy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zlib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zstd.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_barrier.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_semaphore.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_rw_lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/coroutine.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/delayed_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/execution_stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fls.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/notify_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_alarm.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_yielder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/pollable_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/profiling_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/propagating_storage.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/quantized_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/suspendable_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/system_invokers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_affinity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/throughput_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/lease_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/fluent_log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/logger_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/serializable_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_writer_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/file_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/random_access_gzip.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/zstd_compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/arithmetic_formula.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bitmap.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packed_unsigned_vector.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/blob_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bloom_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/checksum.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/coro_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/crash_handler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/digest.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/dnf.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error_code.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ema_counter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/fs.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hazard_ptr.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hedging_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/histogram.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/historic_usage_aggregator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hr_timer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/id_generator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/linear_probe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_reference_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_usage_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/relaxed_mpsc_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/parser_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pattern_formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/phoenix.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pool_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/proc.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/protobuf_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_profiler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/shutdown.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/signal_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/slab_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/cache_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/utf8_decoder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/zerocopy_output_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/dialer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/listener.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/local_address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/socket.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/ares_dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/profiling/timing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authentication_identity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authenticator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/balancing_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/caching_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/channel_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dynamic_channel_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/hedging_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/null_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/per_user_request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/protocol_version.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/response_keeper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/retrying_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/roaming_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/serialized_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/server_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/static_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/throttling_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/viable_peer_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/service_discovery/service_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/spin_wait_slow_path_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_tags.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/utilex/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/forwarding_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/lexer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/null_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_unknown_fields.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser_deserialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/syntax_checker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_merger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/ypath_designated_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/depth_limiting_yson_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attributes_stripper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/convert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_attribute_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_node_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/exception_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/interned_attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/permission.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/request_complexity_limiter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/static_service_dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/system_attribute_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_builder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_visitor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/virtual.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/service_combiner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_serializable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_callbacks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/bindings.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/statistics_producer.cpp
+)
+
+add_global_library_for(yt-yt-core.global yt-yt-core)
+target_compile_options(yt-yt-core.global PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-yt-core.global PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-libunwind
+)
+target_sources(yt-yt-core.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/assert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/guid.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_tracked.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_hooks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/trace_context.cpp
+)
diff --git a/yt/yt/core/CMakeLists.linux-x86_64.txt b/yt/yt/core/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..2a9b71406e
--- /dev/null
+++ b/yt/yt/core/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,368 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+find_package(ZLIB REQUIRED)
+find_package(c-ares REQUIRED)
+add_subdirectory(crypto)
+add_subdirectory(http)
+add_subdirectory(https)
+add_subdirectory(misc)
+
+add_library(yt-yt-core)
+target_compile_options(yt-yt-core PRIVATE
+ -mpclmul
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-yt-core PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-libunwind
+)
+target_sources(yt-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/cancelable_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/current_invoker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/future.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_util.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/packet.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/ssl_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/brotli.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/bzip2.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/codec.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lz.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lzma.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/snappy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zlib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zstd.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_barrier.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_semaphore.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_rw_lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/coroutine.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/delayed_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/execution_stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fls.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/notify_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_alarm.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_yielder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/pollable_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/profiling_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/propagating_storage.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/quantized_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/suspendable_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/system_invokers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_affinity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/throughput_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/lease_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/fluent_log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/logger_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/serializable_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_writer_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/file_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/random_access_gzip.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/zstd_compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/arithmetic_formula.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bitmap.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packed_unsigned_vector.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/blob_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bloom_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/checksum.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/coro_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/crash_handler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/digest.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/dnf.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error_code.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ema_counter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/fs.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hazard_ptr.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hedging_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/histogram.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/historic_usage_aggregator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hr_timer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/id_generator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/linear_probe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_reference_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_usage_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/relaxed_mpsc_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/parser_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pattern_formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/phoenix.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pool_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/proc.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/protobuf_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_profiler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/shutdown.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/signal_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/slab_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/cache_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/utf8_decoder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/zerocopy_output_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/dialer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/listener.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/local_address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/socket.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/ares_dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/profiling/timing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authentication_identity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authenticator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/balancing_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/caching_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/channel_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dynamic_channel_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/hedging_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/null_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/per_user_request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/protocol_version.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/response_keeper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/retrying_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/roaming_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/serialized_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/server_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/static_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/throttling_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/viable_peer_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/service_discovery/service_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/spin_wait_slow_path_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_tags.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/utilex/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/forwarding_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/lexer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/null_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_unknown_fields.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser_deserialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/syntax_checker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_merger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/ypath_designated_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/depth_limiting_yson_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attributes_stripper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/convert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_attribute_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_node_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/exception_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/interned_attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/permission.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/request_complexity_limiter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/static_service_dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/system_attribute_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_builder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_visitor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/virtual.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/service_combiner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_serializable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_callbacks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/bindings.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/statistics_producer.cpp
+)
+
+add_global_library_for(yt-yt-core.global yt-yt-core)
+target_compile_options(yt-yt-core.global PRIVATE
+ -mpclmul
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-yt-core.global PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-libunwind
+)
+target_sources(yt-yt-core.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/assert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/guid.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_tracked.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_hooks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/trace_context.cpp
+)
diff --git a/yt/yt/core/CMakeLists.txt b/yt/yt/core/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/core/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/core/CMakeLists.windows-x86_64.txt b/yt/yt/core/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..a658dc6e7c
--- /dev/null
+++ b/yt/yt/core/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,364 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+find_package(ZLIB REQUIRED)
+find_package(c-ares REQUIRED)
+add_subdirectory(crypto)
+add_subdirectory(http)
+add_subdirectory(https)
+add_subdirectory(misc)
+
+add_library(yt-yt-core)
+target_compile_options(yt-yt-core PRIVATE
+ -mpclmul
+)
+target_link_libraries(yt-yt-core PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-dummy
+)
+target_sources(yt-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/cancelable_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/current_invoker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/future.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/actions/invoker_util.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/dispatcher_impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/packet.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/bus/tcp/ssl_context.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/brotli.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/bzip2.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/codec.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lz.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/lzma.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/snappy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zlib.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/compression/zstd.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_barrier.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_semaphore.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_rw_lock.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/async_stream_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/coroutine.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/delayed_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/execution_stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fair_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fiber_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/fls.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/notify_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_alarm.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/invoker_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/periodic_yielder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/pollable_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/profiling_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/propagating_storage.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/quantized_executor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/suspendable_action_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/system_invokers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_affinity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/thread_pool_poller.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/throughput_throttler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/concurrency/lease_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/fluent_log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/logger_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/serializable_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log_writer_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/file_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/stream_log_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/random_access_gzip.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/zstd_compression.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/arithmetic_formula.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/backoff_strategy_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bitmap.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packed_unsigned_vector.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bit_packing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/blob_output.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/bloom_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/checksum.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/coro_pipe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/crash_handler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/digest.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/dnf.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/error_code.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ema_counter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/fs.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hazard_ptr.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hedging_manager.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/histogram.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/historic_usage_aggregator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/hr_timer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/id_generator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/linear_probe.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_reference_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/memory_usage_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/relaxed_mpsc_queue.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/parser_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pattern_formatter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/phoenix.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/pool_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/proc.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/protobuf_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_counted_tracker_profiler.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/shutdown.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/signal_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/slab_allocator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/statistics.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/cache_config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/utf8_decoder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/zerocopy_output_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/connection.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/dialer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/listener.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/local_address.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/net/socket.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/ares_dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/dns/dns_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/profiling/timing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authentication_identity.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/authenticator.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/balancing_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/caching_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/channel_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/dynamic_channel_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/hedging_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/local_server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/message_format.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/null_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/per_user_request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/protocol_version.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/request_queue_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/response_keeper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/retrying_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/roaming_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/serialized_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/server_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/service_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/static_channel_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/throttling_channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/viable_peer_registry.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/rpc/bus/channel.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/service_discovery/service_discovery.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/spin_wait_slow_path_logger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/threading/thread.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_tags.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/utilex/random.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/stack.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ypath/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/async_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/forwarding_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/lexer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/null_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_options.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/protobuf_interop_unknown_fields.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/pull_parser_deserialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/syntax_checker.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/token_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/tokenizer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/string_merger.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/ypath_designated_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/depth_limiting_yson_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/yson/attributes_stripper.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_consumer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/attribute_filter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/convert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_attribute_owner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ephemeral_node_factory.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/exception_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/interned_attributes.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/node_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/permission.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/request_complexity_limiter.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/serialize.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/static_service_dispatcher.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/system_attribute_provider.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_builder.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/tree_visitor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/virtual.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/service_combiner.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_resolver.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/ypath_service.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_serializable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytree/yson_struct_detail.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_callbacks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_parser.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/json/json_writer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/bindings.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/ytalloc/statistics_producer.cpp
+)
+
+add_global_library_for(yt-yt-core.global yt-yt-core)
+target_compile_options(yt-yt-core.global PRIVATE
+ -mpclmul
+)
+target_link_libraries(yt-yt-core.global PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-snappy
+ ZLIB::ZLIB
+ contrib-libs-zstd
+ contrib-libs-lzmasdk
+ contrib-libs-libbz2
+ c-ares::c-ares
+ contrib-libs-farmhash
+ contrib-libs-yajl
+ contrib-libs-lz4
+ OpenSSL::OpenSSL
+ cpp-openssl-init
+ cpp-threading-thread_local
+ cpp-streams-brotli
+ cpp-yt-assert
+ cpp-yt-containers
+ cpp-yt-logging
+ yt-logging-plain_text_formatter
+ cpp-yt-misc
+ cpp-yt-memory
+ cpp-yt-string
+ cpp-yt-yson
+ cpp-yt-yson_string
+ cpp-ytalloc-api
+ yt-yt-build
+ isa-l_crc_yt_patch
+ yt_proto-yt-core
+ cpp-yt-backtrace
+ cpp-yt-coding
+ cpp-yt-malloc
+ cpp-yt-small_containers
+ cpp-yt-system
+ cpp-yt-threading
+ yt-library-syncmap
+ yt-library-undumpable
+ library-ytprof-api
+ yt-library-profiling
+ library-profiling-resource_tracker
+ yt-library-tracing
+ backtrace-cursors-dummy
+)
+target_sources(yt-yt-core.global PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/logging/log.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/assert.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/guid.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/ref_tracked.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/allocation_hooks.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/tracing/trace_context.cpp
+)
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..b77c8f47db
--- /dev/null
+++ b/yt/yt/core/actions/future.cpp
@@ -0,0 +1,262 @@
+#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.
+ if ([&] {
+ auto guard = Guard(SpinLock_);
+ if (ReadyEvent_ || HasHandlers_ || Canceled_) {
+ return false;
+ }
+ YT_ASSERT(!AbandonedUnset_);
+ AbandonedUnset_ = true;
+ // Cannot access this after UnrefFuture; in particular, cannot touch SpinLock_ in guard's dtor.
+ guard.Release();
+ UnrefFuture();
+ return true;
+ }())
+ {
+ 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..b70c177e3e
--- /dev/null
+++ b/yt/yt/core/actions/invoker.h
@@ -0,0 +1,99 @@
+#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;
+
+ using TWaitTimeObserver = std::function<void(TDuration)>;
+ virtual void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver) = 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..cf64856708
--- /dev/null
+++ b/yt/yt/core/actions/invoker_detail.cpp
@@ -0,0 +1,73 @@
+#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();
+}
+
+void TInvokerWrapper::RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver)
+{
+ return UnderlyingInvoker_->RegisterWaitTimeObserver(waitTimeObserver);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+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..33a293c422
--- /dev/null
+++ b/yt/yt/core/actions/invoker_detail.h
@@ -0,0 +1,55 @@
+#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;
+ void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver) 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..1e8cdbb36e
--- /dev/null
+++ b/yt/yt/core/actions/invoker_util.cpp
@@ -0,0 +1,193 @@
+#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;
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver /*waitTimeObserver*/) override
+ { }
+
+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;
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver /*waitTimeObserver*/) override
+ { }
+};
+
+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/bind_ut.cpp b/yt/yt/core/actions/unittests/bind_ut.cpp
new file mode 100644
index 0000000000..a44dd05f0c
--- /dev/null
+++ b/yt/yt/core/actions/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/actions/unittests/future_ut.cpp b/yt/yt/core/actions/unittests/future_ut.cpp
new file mode 100644
index 0000000000..ee676f7010
--- /dev/null
+++ b/yt/yt/core/actions/unittests/future_ut.cpp
@@ -0,0 +1,1634 @@
+#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();
+}
+
+TEST_F(TFutureTest, AbandonBeforeGet)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ promise.Reset();
+ EXPECT_EQ(future.Get().GetCode(), EErrorCode::Canceled);
+}
+
+TEST_F(TFutureTest, AbandonDuringGet)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ std::thread thread([&] {
+ Sleep(TDuration::MilliSeconds(100));
+ promise.Reset();
+ });
+ EXPECT_EQ(future.Get().GetCode(), EErrorCode::Canceled);
+ thread.join();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // 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..d98bf65c70
--- /dev/null
+++ b/yt/yt/core/actions/unittests/ya.make
@@ -0,0 +1,47 @@
+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
+ bind_ut.cpp
+ future_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..2506257906
--- /dev/null
+++ b/yt/yt/core/bus/bus.h
@@ -0,0 +1,172 @@
+#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 true if the bus' communication is encrypted.
+ virtual bool IsEncrypted() 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..f5359d51b7
--- /dev/null
+++ b/yt/yt/core/bus/public.h
@@ -0,0 +1,62 @@
+#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))
+ ((SslError) (119))
+);
+
+DEFINE_ENUM(EEncryptionMode,
+ ((Disabled) (0))
+ ((Optional) (1))
+ ((Required) (2))
+);
+
+DEFINE_ENUM(EVerificationMode,
+ ((None) (0)) // Do no certificate or host name verifications.
+ ((Ca) (1)) // Verifies peer's certificate with the CA.
+ ((Full) (2)) // Verifies peer's certificate with the CA as well as peer's host name against the certificate.
+);
+
+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..84b60f63b9
--- /dev/null
+++ b/yt/yt/core/bus/tcp/client.cpp
@@ -0,0 +1,229 @@
+#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();
+ }
+
+ bool IsEncrypted() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->IsEncrypted();
+ }
+
+ 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, EncryptionMode: %v)",
+ EndpointDescription_,
+ id,
+ options.MultiplexingBand,
+ Config_->EncryptionMode);
+
+ 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..ed0074651f
--- /dev/null
+++ b/yt/yt/core/bus/tcp/config.cpp
@@ -0,0 +1,160 @@
+#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);
+ registrar.Parameter("encryption_mode", &TThis::EncryptionMode)
+ .Default(EEncryptionMode::Optional);
+ registrar.Parameter("verification_mode", &TThis::VerificationMode)
+ .Default(EVerificationMode::None);
+ registrar.Parameter("ca_file", &TThis::CAFile)
+ .Default();
+ registrar.Parameter("cert_chain_file", &TThis::CertificateChainFile)
+ .Default();
+ registrar.Parameter("private_key_file", &TThis::PrivateKeyFile)
+ .Default();
+ registrar.Parameter("cipher_list", &TThis::CipherList)
+ .Default();
+ registrar.Parameter("use_key_pair_from_ssl_context", &TThis::UseKeyPairFromSslContext)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+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..b086553743
--- /dev/null
+++ b/yt/yt/core/bus/tcp/config.h
@@ -0,0 +1,150 @@
+#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;
+
+ // Ssl options.
+ EEncryptionMode EncryptionMode;
+ EVerificationMode VerificationMode;
+ std::optional<TString> CAFile;
+ std::optional<TString> CertificateChainFile;
+ std::optional<TString> PrivateKeyFile;
+ std::optional<TString> CipherList;
+ // For testing purposes.
+ bool UseKeyPairFromSslContext;
+
+ 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..9044c8283c
--- /dev/null
+++ b/yt/yt/core/bus/tcp/connection.cpp
@@ -0,0 +1,2044 @@
+#include "connection.h"
+
+#include "config.h"
+#include "server.h"
+#include "dispatcher_impl.h"
+#include "ssl_context.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>
+#include <util/system/hostname.h>
+
+#ifdef __linux__
+#include <netinet/tcp.h>
+#endif
+
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+
+#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 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 auto SslAckPacketId = TPacketId(2, 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());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTcpConnection::TDeleter::operator()(SSL* ctx) const
+{
+ SSL_free(ctx);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+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, ConnectionType: %v, RemoteAddress: %v, EncryptionMode: %v",
+ Id_,
+ ConnectionType_,
+ EndpointDescription_,
+ Config_->EncryptionMode))
+ , 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())
+ , EncryptionMode_(Config_->EncryptionMode)
+ , VerificationMode_(Config_->VerificationMode)
+{ }
+
+TTcpConnection::~TTcpConnection()
+{
+ Close();
+}
+
+void TTcpConnection::Close()
+{
+ CloseSslSession(ESslState::Closed);
+
+ {
+ 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()
+{
+ YT_LOG_DEBUG("Starting TCP connection (SupportsHandshakes: %v)", SupportsHandshakes_);
+
+ // 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();
+
+ if (!SupportsHandshakes_) {
+ // Set the connection ready on the server side.
+ 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::TryEnqueueHandshake()
+{
+ if (!SupportsHandshakes_) {
+ return;
+ }
+
+ if (std::exchange(HandshakeEnqueued_, true)) {
+ return;
+ }
+
+ NProto::THandshake handshake;
+ ToProto(handshake.mutable_foreign_connection_id(), Id_);
+ if (ConnectionType_ == EConnectionType::Client) {
+ handshake.set_multiplexing_band(ToProto<int>(MultiplexingBand_.load()));
+ }
+ handshake.set_encryption_mode(static_cast<int>(EncryptionMode_));
+
+ 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("TCP connection has been 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;
+ try {
+ EndpointHostName_ = FQDNHostName();
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Failed to resolve local host name");
+ EndpointHostName_ = "localhost";
+ }
+
+ // 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;
+ }
+
+ EndpointHostName_ = hostName;
+
+ 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_, IsEncrypted());
+
+ // Suppress checksum generation for local traffic.
+ if (NetworkName_ == LocalNetworkName) {
+ GenerateChecksums_ = false;
+ }
+}
+
+void TTcpConnection::Abort(const TError& error)
+{
+ AbortSslSession();
+
+ // 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;
+ }
+
+ YT_LOG_DEBUG("Aborting connection since networking is disabled");
+
+ 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)
+{
+ YT_LOG_DEBUG("Dialer finished");
+
+ 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();
+ }
+
+ if (!SupportsHandshakes_) {
+ // Set the connection ready on the client side.
+ 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;
+}
+
+bool TTcpConnection::IsEncrypted() const
+{
+ return SslState_ != ESslState::None;
+}
+
+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");
+
+ // Proceed with pending ssl handshake prior to reads or writes.
+ if (PendingSslHandshake_) {
+ PendingSslHandshake_ = DoSslHandshake();
+ }
+
+ // 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 (ConnectionType_ == EConnectionType::Client) {
+ // Client initiates a handshake.
+ TryEnqueueHandshake();
+ }
+ 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");
+
+ bool readOnlySslAckPacket = RemainingSslAckPacketBytes_ > 0;
+
+ 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);
+
+ if (RemainingSslAckPacketBytes_ > 0) {
+ bytesToRead = std::min(bytesToRead, RemainingSslAckPacketBytes_);
+ }
+
+ YT_LOG_TRACE("Reading from socket into decoder (BytesToRead: %v)",bytesToRead);
+
+ size_t bytesRead;
+ if (!ReadSocket(decoderChunk.Begin(), bytesToRead, &bytesRead)) {
+ break;
+ }
+
+ bytesReadTotal += bytesRead;
+
+ if (RemainingSslAckPacketBytes_ > 0) {
+ RemainingSslAckPacketBytes_ -= bytesRead;
+ }
+
+ if (!AdvanceDecoder(bytesRead)) {
+ return;
+ }
+ } else {
+ // Read a chunk into the read buffer.
+ size_t bytesToRead = ReadBuffer_.Size();
+
+ if (RemainingSslAckPacketBytes_ > 0) {
+ bytesToRead = std::min(bytesToRead, RemainingSslAckPacketBytes_);
+ }
+
+ YT_LOG_TRACE("Reading from socket into buffer (BytesToRead: %v)", bytesToRead);
+
+ size_t bytesRead;
+ if (!ReadSocket(ReadBuffer_.Begin(), bytesToRead, &bytesRead)) {
+ break;
+ }
+
+ bytesReadTotal += bytesRead;
+
+ if (RemainingSslAckPacketBytes_ > 0) {
+ RemainingSslAckPacketBytes_ -= 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");
+ }
+
+ if (readOnlySslAckPacket && RemainingSslAckPacketBytes_ == 0) {
+ break;
+ }
+ }
+
+ 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();
+}
+
+ssize_t TTcpConnection::DoReadSocket(char* buffer, size_t size)
+{
+ switch (SslState_) {
+ case ESslState::None:
+ return HandleEintr(recv, Socket_, buffer, size, 0);
+ case ESslState::Established: {
+ auto result = SSL_read(Ssl_.get(), buffer, size);
+ if (PendingSslHandshake_ && result > 0) {
+ YT_LOG_DEBUG("TLS/SSL connection has been established by SSL_read (VerificationMode: %v)", VerificationMode_);
+ PendingSslHandshake_ = false;
+ ReadyPromise_.TrySet();
+ }
+ return result;
+ }
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+bool TTcpConnection::ReadSocket(char* buffer, size_t size, size_t* bytesRead)
+{
+ NProfiling::TWallTimer timer;
+ auto result = DoReadSocket(buffer, size);
+ 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::CheckTcpReadError(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::CheckReadError(ssize_t result)
+{
+ if (SslState_ == ESslState::None) {
+ return CheckTcpReadError(result);
+ }
+
+ return CheckSslReadError(result);
+}
+
+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::SslAck:
+ return OnSslAckPacketReceived();
+ case EPacketType::Message:
+ return OnMessagePacketReceived();
+ default:
+ YT_LOG_ERROR("Packet of unknown type received, ignored (PacketId: %v, PacketType: %v)",
+ Decoder_->GetPacketId(),
+ Decoder_->GetPacketType());
+ break;
+ }
+
+ return false;
+}
+
+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()
+{
+ YT_LOG_DEBUG("Handshake received");
+
+ 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, ForeignEncryptionMode: %v)",
+ FromProto<TConnectionId>(handshake.foreign_connection_id()),
+ optionalMultiplexingBand,
+ static_cast<EEncryptionMode>(handshake.encryption_mode()));
+
+ if (ConnectionType_ == EConnectionType::Server && optionalMultiplexingBand) {
+ auto guard = Guard(Lock_);
+ UpdateConnectionCount(-1);
+ MultiplexingBand_.store(*optionalMultiplexingBand);
+ UpdateConnectionCount(+1);
+ }
+
+ HandshakeReceived_ = true;
+
+ if (ConnectionType_ == EConnectionType::Server) {
+ // Server responds to client's handshake.
+ TryEnqueueHandshake();
+ }
+
+ auto otherEncryptionMode = handshake.has_encryption_mode() ? static_cast<EEncryptionMode>(handshake.encryption_mode()) : EEncryptionMode::Disabled;
+
+ if (EncryptionMode_ == EEncryptionMode::Required || otherEncryptionMode == EEncryptionMode::Required) {
+ if (EncryptionMode_ == EEncryptionMode::Disabled || otherEncryptionMode == EEncryptionMode::Disabled) {
+ Abort(TError(NBus::EErrorCode::SslError, "TLS/SSL client/server encryption mode compatibility error")
+ << TErrorAttribute("mode", EncryptionMode_)
+ << TErrorAttribute("other_mode", otherEncryptionMode));
+ } else {
+ EstablishSslSession_ = true;
+ // Expect ssl ack from the other side.
+ RemainingSslAckPacketBytes_ = GetSslAckPacketSize();
+ if (ConnectionType_ == EConnectionType::Client) {
+ // Client initiates ssl ack.
+ TryEnqueueSslAck();
+ }
+
+ // ReadyPromise_ is set either after successful establishment of ssl session or in Abort().
+ }
+ } else {
+ ReadyPromise_.TrySet();
+ }
+
+ 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();
+}
+
+ssize_t TTcpConnection::DoWriteFragments(const std::vector<struct iovec>& vec)
+{
+ if (vec.empty()) {
+ return 0;
+ }
+
+ switch (SslState_) {
+ case ESslState::None:
+ return HandleEintr(::writev, Socket_, vec.data(), vec.size());
+ case ESslState::Established: {
+ YT_ASSERT(vec.size() == 1);
+ auto result = SSL_write(Ssl_.get(), vec[0].iov_base, vec[0].iov_len);
+ if (PendingSslHandshake_ && result > 0) {
+ YT_LOG_DEBUG("TLS/SSL connection has been established by SSL_write (VerificationMode: %v)", VerificationMode_);
+ PendingSslHandshake_ = false;
+ ReadyPromise_.TrySet();
+ }
+ return result;
+ }
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+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 = DoWriteFragments(SendVector_);
+ 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::CheckTcpWriteError(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;
+}
+
+bool TTcpConnection::CheckWriteError(ssize_t result)
+{
+ if (SslState_ == ESslState::None) {
+ return CheckTcpWriteError(result);
+ }
+
+ return CheckSslWriteError(result);
+}
+
+void TTcpConnection::OnPacketSent()
+{
+ const auto& packet = EncodedPackets_.front();
+ switch (packet->Type) {
+ case EPacketType::Ack:
+ OnAckPacketSent(*packet);
+ break;
+
+ case EPacketType::SslAck:
+ OnSslAckPacketSent();
+ 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");
+
+ HandshakeSent_ = true;
+}
+
+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
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTcpConnection::AbortSslSession()
+{
+ CloseSslSession(ESslState::Aborted);
+}
+
+bool TTcpConnection::CheckSslReadError(ssize_t result)
+{
+ switch (SSL_get_error(Ssl_.get(), result)) {
+ case SSL_ERROR_NONE:
+ return true;
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ // Try again.
+ break;
+ case SSL_ERROR_SYSCALL:
+ case SSL_ERROR_SSL:
+ // This check is probably unnecessary in new versions of openssl.
+ if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
+ break;
+ }
+ SslState_ = ESslState::Error;
+ [[fallthrough]];
+ default:
+ UpdateBusCounter(&TBusNetworkBandCounters::ReadErrors, 1);
+ Abort(TError(NBus::EErrorCode::SslError, "TLS/SSL read error")
+ << TErrorAttribute("ssl_error", GetLastSslErrorString())
+ << TErrorAttribute("sys_error", TError::FromSystem(LastSystemError())));
+ break;
+ }
+
+ return false;
+}
+
+bool TTcpConnection::CheckSslWriteError(ssize_t result)
+{
+ switch (SSL_get_error(Ssl_.get(), result)) {
+ case SSL_ERROR_NONE:
+ return true;
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ // Try again.
+ break;
+ case SSL_ERROR_SYSCALL:
+ case SSL_ERROR_SSL:
+ // This check is probably unnecessary in new versions of openssl.
+ if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
+ break;
+ }
+ SslState_ = ESslState::Error;
+ [[fallthrough]];
+ default:
+ UpdateBusCounter(&TBusNetworkBandCounters::WriteErrors, 1);
+ Abort(TError(NBus::EErrorCode::SslError, "TLS/SSL write error")
+ << TErrorAttribute("ssl_error", GetLastSslErrorString())
+ << TErrorAttribute("sys_error", TError::FromSystem(LastSystemError())));
+ break;
+ }
+
+ return false;
+}
+
+void TTcpConnection::CloseSslSession(ESslState newSslState)
+{
+ switch (SslState_) {
+ case ESslState::None:
+ case ESslState::Aborted:
+ case ESslState::Closed:
+ // Nothing to do.
+ return;
+ case ESslState::Established:
+ SSL_shutdown(Ssl_.get());
+ break;
+ case ESslState::Error:
+ break;
+ default:
+ YT_ABORT();
+ }
+
+ SslState_ = newSslState;
+}
+
+bool TTcpConnection::DoSslHandshake()
+{
+ auto result = SSL_do_handshake(Ssl_.get());
+ switch (SSL_get_error(Ssl_.get(), result)) {
+ case SSL_ERROR_NONE:
+ YT_LOG_DEBUG("TLS/SSL connection has been established by SSL_do_handshake (VerificationMode %v)", VerificationMode_);
+ MaxFragmentsPerWrite = 1;
+ SslState_ = ESslState::Established;
+ ReadyPromise_.TrySet();
+ return false;
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ MaxFragmentsPerWrite = 1;
+ SslState_ = ESslState::Established;
+ // Ssl session establishment will be finished by the following SSL_do_handshake()/SSL_read()/SSL_write() calls.
+ // Since SSL handshake is pending, don't set the ReadyPromise_ yet.
+ return true;
+ case SSL_ERROR_ZERO_RETURN:
+ // The TLS/SSL peer has closed the TLS/SSL session.
+ break;
+ case SSL_ERROR_SYSCALL:
+ case SSL_ERROR_SSL:
+ SslState_ = ESslState::Error;
+ break;
+ default:
+ // Use default handler for other error types.
+ break;
+ }
+
+ Abort(TError(NBus::EErrorCode::SslError, "Failed to establish TLS/SSL session")
+ << TErrorAttribute("ssl_error", GetLastSslErrorString())
+ << TErrorAttribute("sys_error", TError::FromSystem(LastSystemError())));
+ return false;
+}
+
+size_t TTcpConnection::GetSslAckPacketSize()
+{
+ return Encoder_->GetPacketSize(EPacketType::SslAck, {}, 0);
+}
+
+void TTcpConnection::TryEnqueueSslAck()
+{
+ if (!HandshakeSent_ || !HandshakeReceived_ || !EstablishSslSession_) {
+ return;
+ }
+
+ if (std::exchange(SslAckEnqueued_, true)) {
+ return;
+ }
+
+ EnqueuePacket(
+ // COMPAT(babenko)
+ EPacketType::SslAck,
+ EPacketFlags::None,
+ 0,
+ SslAckPacketId);
+
+ YT_LOG_DEBUG("SslAck enqueued");
+}
+
+void TTcpConnection::TryEstablishSslSession()
+{
+ if (!SslAckReceived_ || !SslAckSent_ || Ssl_) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Starting TLS/SSL connection (VerificationMode: %v)", VerificationMode_);
+
+ Ssl_.reset(SSL_new(TSslContext::Get()->GetSslCtx()));
+ if (!Ssl_) {
+ Abort(TError(NBus::EErrorCode::SslError, "Failed to create a new SSL structure: %v", GetLastSslErrorString()));
+ return;
+ }
+
+ if (SSL_set_fd(Ssl_.get(), Socket_) != 1) {
+ Abort(TError(NBus::EErrorCode::SslError, "Failed to bind socket to SSL handle: %v", GetLastSslErrorString()));
+ return;
+ }
+
+ if (ConnectionType_ == EConnectionType::Server) {
+ SSL_set_accept_state(Ssl_.get());
+
+ if (!Config_->UseKeyPairFromSslContext) {
+ if (!Config_->CertificateChainFile) {
+ Abort(TError(NBus::EErrorCode::SslError, "The certificate chain file is not set in bus config"));
+ return;
+ }
+
+ if (SSL_use_certificate_chain_file(Ssl_.get(), Config_->CertificateChainFile->data()) != 1) {
+ Abort(TError(NBus::EErrorCode::SslError, "Failed to load certificate chain: %v", GetLastSslErrorString()));
+ return;
+ }
+
+ if (!Config_->PrivateKeyFile) {
+ Abort(TError(NBus::EErrorCode::SslError, "The private key file is not set in bus config"));
+ return;
+ }
+
+ if (SSL_use_RSAPrivateKey_file(Ssl_.get(), Config_->PrivateKeyFile->data(), SSL_FILETYPE_PEM) != 1) {
+ Abort(TError(NBus::EErrorCode::SslError, "Failed to load private key: %v", GetLastSslErrorString()));
+ return;
+ }
+
+ if (SSL_check_private_key(Ssl_.get()) != 1) {
+ Abort(TError(NBus::EErrorCode::SslError, "Failed to check the consistency of a private key with the corresponding certificate: %v", GetLastSslErrorString()));
+ return;
+ }
+ }
+ } else {
+ SSL_set_connect_state(Ssl_.get());
+ }
+
+ switch (VerificationMode_) {
+ case EVerificationMode::Full:
+ // Set the hostname for the peer certificate verification.
+ if (SSL_set1_host(Ssl_.get(), EndpointHostName_.data()) != 1) {
+ Abort(TError(NBus::EErrorCode::SslError, "Failed to set hostname %v for the peer certificate verification", EndpointHostName_));
+ return;
+ }
+ [[fallthrough]];
+ case EVerificationMode::Ca:
+ if (!Config_->CAFile) {
+ Abort(TError(NBus::EErrorCode::SslError, "The CA file is not set in bus config"));
+ return;
+ }
+ TSslContext::Get()->LoadCAFileIfNotLoaded(*Config_->CAFile);
+
+ // Enable verification of the peer's certificate with the CA.
+ SSL_set_verify(Ssl_.get(), SSL_VERIFY_PEER, /* callback */ nullptr);
+ break;
+ case EVerificationMode::None:
+ break;
+ default:
+ YT_ABORT();
+ }
+
+ PendingSslHandshake_ = DoSslHandshake();
+
+ // Check that connection hasn't been aborted in DoSslHandshake().
+ if (State_ == EState::Open) {
+ UpdateConnectionCount(-1);
+ FlushBusStatistics();
+ NetworkCounters_.Exchange(TTcpDispatcher::TImpl::Get()->GetCounters(NetworkName_, IsEncrypted()));
+ UpdateConnectionCount(1);
+ }
+}
+
+bool TTcpConnection::OnSslAckPacketReceived()
+{
+ YT_LOG_DEBUG("SslAck received");
+
+ SslAckReceived_ = true;
+
+ if (ConnectionType_ == EConnectionType::Server) {
+ // Server responds to client's ssl ack.
+ TryEnqueueSslAck();
+ }
+
+ TryEstablishSslSession();
+
+ return true;
+}
+
+void TTcpConnection::OnSslAckPacketSent()
+{
+ YT_LOG_DEBUG("SslAck sent");
+
+ SslAckSent_ = true;
+
+ TryEstablishSslSession();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..972c1d7972
--- /dev/null
+++ b/yt/yt/core/bus/tcp/connection.h
@@ -0,0 +1,385 @@
+#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>
+
+#include <openssl/ssl.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)
+);
+
+DEFINE_ENUM(ESslSessionState,
+ (None)
+ (Established)
+ (Error)
+ (Closed)
+ (Aborted)
+);
+
+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;
+ bool IsEncrypted() 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;
+
+ using ESslState = ESslSessionState;
+
+ 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>;
+
+ struct TDeleter
+ {
+ void operator()(SSL* ctx) const;
+ };
+
+ 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_;
+ // Endpoint host name is used for peer's certificate verification.
+ TString EndpointHostName_;
+
+ 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;
+ bool HandshakeReceived_ = false;
+ bool HandshakeSent_ = false;
+
+ // How many bytes of SSL ACK packet remains to be read.
+ size_t RemainingSslAckPacketBytes_ = 0;
+ // TLS/SSL connection needs to be set up.
+ bool EstablishSslSession_ = false;
+ // TLS/SSL handshake has been started but hasn't been completed yet.
+ bool PendingSslHandshake_ = false;
+ bool SslAckEnqueued_ = false;
+ bool SslAckReceived_ = false;
+ bool SslAckSent_ = false;
+ std::unique_ptr<SSL, TDeleter> Ssl_;
+ std::atomic<ESslState> SslState_ = ESslState::None;
+
+ const EEncryptionMode EncryptionMode_;
+ const EVerificationMode VerificationMode_;
+
+ size_t MaxFragmentsPerWrite = 256;
+
+ void Open();
+ void Close();
+ void CloseSslSession(ESslState newSslState);
+
+ void Abort(const TError& error);
+ bool AbortIfNetworkingDisabled();
+ void AbortSslSession();
+
+ 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();
+ bool OnSslAckPacketReceived();
+
+ 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 OnSslAckPacketSent();
+ void OnTerminate();
+ void ProcessQueuedMessages();
+ void DiscardOutcomingMessages();
+ void DiscardUnackedMessages();
+
+ void TryEnqueueHandshake();
+ void TryEnqueueSslAck();
+ 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);
+
+ bool CheckSslReadError(ssize_t result);
+ bool CheckSslWriteError(ssize_t result);
+ bool CheckTcpReadError(ssize_t result);
+ bool CheckTcpWriteError(ssize_t result);
+ bool DoSslHandshake();
+ size_t GetSslAckPacketSize();
+ void TryEstablishSslSession();
+
+ ssize_t DoReadSocket(char* buffer, size_t size);
+ ssize_t DoWriteFragments(const std::vector<struct iovec>& vec);
+};
+
+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..1c4b368cfe
--- /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, bool encrypted)
+{
+ return Impl_->GetCounters(networkName, encrypted);
+}
+
+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..b4a4a61580
--- /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, bool encrypted);
+
+ //! 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..5accc4d1d0
--- /dev/null
+++ b/yt/yt/core/bus/tcp/dispatcher_impl.cpp
@@ -0,0 +1,326 @@
+#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, bool encrypted)
+{
+ auto [statistics, ok] = NetworkStatistics_.FindOrInsert(networkName, [] {
+ return std::array<TNetworkStatistics, 2>{};
+ });
+
+ return (*statistics)[encrypted].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) {
+ TWithTagGuard networkTagGuard(writer, "network", name);
+
+ for (auto encrypted : { false, true }) {
+ TWithTagGuard encryptedTagGuard(writer, "encrypted", ToString(encrypted));
+
+ const auto& counters = statistics[encrypted].Counters;
+
+ 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("encrypted").Value(connection->IsEncrypted())
+ .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..bc6e059a15
--- /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, bool encrypted);
+
+ 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, std::array<TNetworkStatistics, 2>> 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..93bf292fb0
--- /dev/null
+++ b/yt/yt/core/bus/tcp/packet.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "private.h"
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(EPacketType, i16,
+ ((Message)(0))
+ ((Ack) (1))
+ ((SslAck) (2))
+);
+
+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..aa46ee9068
--- /dev/null
+++ b/yt/yt/core/bus/tcp/server.cpp
@@ -0,0 +1,494 @@
+#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;
+ for (auto encrypted : { false, true }) {
+ const auto& counters = dispatcher->GetCounters(clientNetwork, encrypted);
+ 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/tcp/ssl_context.cpp b/yt/yt/core/bus/tcp/ssl_context.cpp
new file mode 100644
index 0000000000..e2836da542
--- /dev/null
+++ b/yt/yt/core/bus/tcp/ssl_context.cpp
@@ -0,0 +1,294 @@
+#include "ssl_context.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <library/cpp/openssl/init/init.h>
+
+#include <util/system/mutex.h>
+
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+
+#include <memory>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetLastSslErrorString()
+{
+ char errorStr[256];
+ ERR_error_string_n(ERR_get_error(), errorStr, sizeof(errorStr));
+ return errorStr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSslContext::TImpl
+{
+private:
+ struct TDeleter
+ {
+ void operator()(SSL_CTX* ctx) const
+ {
+ SSL_CTX_free(ctx);
+ }
+
+ void operator()(BIO* bio) const
+ {
+ BIO_free(bio);
+ }
+
+ void operator()(RSA* rsa) const
+ {
+ RSA_free(rsa);
+ }
+
+ void operator()(X509* x509) const
+ {
+ X509_free(x509);
+ }
+ };
+
+public:
+ TImpl()
+ {
+ InitOpenSSL();
+
+ SslCtx_.reset(SSL_CTX_new(TLS_method()));
+ if (!SslCtx_) {
+ THROW_ERROR_EXCEPTION("Failed to create TLS/SSL context: %v", GetLastSslErrorString());
+ }
+
+ if (SSL_CTX_set_min_proto_version(SslCtx_.get(), TLS1_2_VERSION) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to set min protocol version: %v", GetLastSslErrorString());
+ }
+
+ if (SSL_CTX_set_max_proto_version(SslCtx_.get(), TLS1_2_VERSION) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to set max protocol version: %v", GetLastSslErrorString());
+ }
+
+ SSL_CTX_set_mode(SslCtx_.get(), SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ }
+
+ SSL_CTX* GetCtx()
+ {
+ return SslCtx_.get();
+ }
+
+ void LoadCAFile(const TString& filePath)
+ {
+ TGuard<TMutex> guard(CAMutex_);
+
+ LoadCAFileUnlocked(filePath);
+
+ CAIsLoaded_ = true;
+ }
+
+ void LoadCAFileIfNotLoaded(const TString& filePath)
+ {
+ if (CAIsLoaded_) {
+ return;
+ }
+
+ TGuard<TMutex> guard(CAMutex_);
+
+ if (CAIsLoaded_) {
+ return;
+ }
+
+ LoadCAFileUnlocked(filePath);
+
+ CAIsLoaded_ = true;
+ }
+
+ void LoadCertificateChain(const TString& filePath)
+ {
+ auto ret = SSL_CTX_use_certificate_chain_file(SslCtx_.get(), filePath.data());
+ if (ret != 1) {
+ THROW_ERROR_EXCEPTION("Failed to load certificate chain: %v", GetLastSslErrorString());
+ }
+ }
+
+ void LoadPrivateKey(const TString& filePath)
+ {
+ auto ret = SSL_CTX_use_RSAPrivateKey_file(SslCtx_.get(), filePath.data(), SSL_FILETYPE_PEM);
+ if (ret != 1) {
+ THROW_ERROR_EXCEPTION("Failed to load private key: %v", GetLastSslErrorString());
+ }
+ }
+
+ void UseCA(const TString& ca)
+ {
+ TGuard<TMutex> guard(CAMutex_);
+
+ UseCAUnlocked(ca);
+
+ CAIsLoaded_ = true;
+ }
+
+ void UseCertificateChain(const TString& certificate)
+ {
+ std::unique_ptr<BIO, TDeleter> bio(BIO_new_mem_buf(certificate.data(), certificate.size()));
+ if (!bio) {
+ THROW_ERROR_EXCEPTION("Failed to allocate memory buffer for certificate: %v", GetLastSslErrorString());
+ }
+
+ // The first certificate in the chain is expected to be a leaf.
+ std::unique_ptr<X509, TDeleter> cert(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
+ if (!cert) {
+ THROW_ERROR_EXCEPTION("Failed to read certificate from memory buffer: %v", GetLastSslErrorString());
+ }
+
+ if (SSL_CTX_use_certificate(SslCtx_.get(), cert.get()) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to use cert in ssl: %v", GetLastSslErrorString());
+ }
+
+ // Load additional certificates in the chain.
+ while (auto cert = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)) {
+ if (SSL_CTX_add0_chain_cert(SslCtx_.get(), cert) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to add cert to ssl: %v", GetLastSslErrorString());
+ }
+ // Do not X509_free() if certificate was added by SSL_CTX_add0_chain_cert().
+ }
+ }
+
+ void UsePrivateKey(const TString& privateKey)
+ {
+ std::unique_ptr<BIO, TDeleter> bio(BIO_new_mem_buf(privateKey.data(), privateKey.size()));
+ if (!bio) {
+ THROW_ERROR_EXCEPTION("Failed to allocate memory buffer for private key: %v", GetLastSslErrorString());
+ }
+
+ std::unique_ptr<RSA, TDeleter> pkey(PEM_read_bio_RSAPrivateKey(bio.get(), nullptr, nullptr, nullptr));
+ if (!pkey) {
+ THROW_ERROR_EXCEPTION("Failed to read private key from memory buffer: %v", GetLastSslErrorString());
+ }
+
+ if (SSL_CTX_use_RSAPrivateKey(SslCtx_.get(), pkey.get()) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to add the private RSA key to ctx: %v", GetLastSslErrorString());
+ }
+ }
+
+ void SetCipherList(const TString& cipherList)
+ {
+ if (SSL_CTX_set_cipher_list(SslCtx_.get(), cipherList.data()) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to set cipher list: %v", GetLastSslErrorString());
+ }
+ }
+
+ //! Check the consistency of a private key with the corresponding certificate.
+ //! A private key and the corresponding certificate have to be loaded into ctx.
+ void CheckPrivateKeyWithCertificate()
+ {
+ if (SSL_CTX_check_private_key(SslCtx_.get()) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to check the consistency of a private key with the corresponding certificate: %v", GetLastSslErrorString());
+ }
+ }
+
+private:
+ void LoadCAFileUnlocked(const TString& filePath)
+ {
+ auto ret = SSL_CTX_load_verify_locations(SslCtx_.get(), filePath.data(), nullptr);
+ if (ret != 1) {
+ THROW_ERROR_EXCEPTION("Failed to load CA file: %v", GetLastSslErrorString());
+ }
+ }
+
+ void UseCAUnlocked(const TString& ca)
+ {
+ std::unique_ptr<BIO, TDeleter> bio(BIO_new_mem_buf(ca.data(), ca.size()));
+ if (!bio) {
+ THROW_ERROR_EXCEPTION("Failed to allocate memory buffer for CA certificate: %v", GetLastSslErrorString());
+ }
+
+ auto store = SSL_CTX_get_cert_store(SslCtx_.get());
+
+ // Load certificate chain.
+ while (auto cert = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)) {
+ if (X509_STORE_add_cert(store, cert) != 1) {
+ THROW_ERROR_EXCEPTION("Failed to add cert to store: %v", GetLastSslErrorString());
+ }
+ X509_free(cert);
+ }
+ }
+
+private:
+ TMutex CAMutex_;
+ std::atomic<bool> CAIsLoaded_ = false;
+ std::unique_ptr<SSL_CTX, TDeleter> SslCtx_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSslContext::TSslContext()
+{
+ Reset();
+}
+
+TSslContext::~TSslContext() = default;
+
+SSL_CTX* TSslContext::GetSslCtx()
+{
+ return Impl_->GetCtx();
+}
+
+TSslContext* TSslContext::Get()
+{
+ return LeakySingleton<TSslContext>();
+}
+
+void TSslContext::LoadCAFile(const TString& filePath)
+{
+ return Impl_->LoadCAFile(filePath);
+}
+
+void TSslContext::LoadCAFileIfNotLoaded(const TString& filePath)
+{
+ return Impl_->LoadCAFileIfNotLoaded(filePath);
+}
+
+void TSslContext::LoadCertificateChain(const TString& filePath)
+{
+ return Impl_->LoadCertificateChain(filePath);
+}
+
+void TSslContext::LoadPrivateKey(const TString& filePath)
+{
+ return Impl_->LoadPrivateKey(filePath);
+}
+
+void TSslContext::UseCA(const TString& ca)
+{
+ return Impl_->UseCA(ca);
+}
+
+void TSslContext::UseCertificateChain(const TString& certificate)
+{
+ return Impl_->UseCertificateChain(certificate);
+}
+
+void TSslContext::UsePrivateKey(const TString& privateKey)
+{
+ return Impl_->UsePrivateKey(privateKey);
+}
+
+void TSslContext::SetCipherList(const TString& cipherList)
+{
+ return Impl_->SetCipherList(cipherList);
+}
+
+void TSslContext::CheckPrivateKeyWithCertificate()
+{
+ return Impl_->CheckPrivateKeyWithCertificate();
+}
+
+void TSslContext::Reset()
+{
+ Impl_ = std::make_unique<TImpl>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/ssl_context.h b/yt/yt/core/bus/tcp/ssl_context.h
new file mode 100644
index 0000000000..635422c086
--- /dev/null
+++ b/yt/yt/core/bus/tcp/ssl_context.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "public.h"
+
+#include <openssl/ssl.h>
+
+#include <memory>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetLastSslErrorString();
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSslContext
+{
+public:
+ TSslContext();
+ ~TSslContext();
+
+ SSL_CTX* GetSslCtx();
+ static TSslContext* Get();
+
+ void LoadCAFile(const TString& filePath);
+ void LoadCAFileIfNotLoaded(const TString& filePath);
+ void LoadCertificateChain(const TString& filePath);
+ void LoadPrivateKey(const TString& filePath);
+ void UseCA(const TString& ca);
+ void UseCertificateChain(const TString& certificate);
+ void UsePrivateKey(const TString& privateKey);
+ void SetCipherList(const TString& cipherList);
+ void CheckPrivateKeyWithCertificate();
+ // For testing purposes.
+ void Reset();
+
+private:
+ class TImpl;
+ std::unique_ptr<TImpl> Impl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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/ssl_ut.cpp b/yt/yt/core/bus/unittests/ssl_ut.cpp
new file mode 100644
index 0000000000..3c96233fb7
--- /dev/null
+++ b/yt/yt/core/bus/unittests/ssl_ut.cpp
@@ -0,0 +1,547 @@
+#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/bus/tcp/ssl_context.h>
+
+#include <library/cpp/testing/common/network.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{});
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEmptyBusHandler
+ : public IMessageHandler
+{
+public:
+ void HandleMessage(
+ TSharedRefArray message,
+ IBusPtr replyBus) noexcept override
+ {
+ Y_UNUSED(message);
+ Y_UNUSED(replyBus);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSslTest
+ : public testing::Test
+{
+public:
+ NTesting::TPortHolder Port;
+ TString Address;
+
+ TSslTest()
+ {
+ Port = NTesting::GetFreePort();
+ Address = Format("localhost:%v", Port);
+
+ LoadKeyPairIntoCxt();
+ }
+
+ void LoadKeyPairIntoCxt()
+ {
+ auto certChain = R"foo(-----BEGIN CERTIFICATE-----
+MIIFUTCCAzmgAwIBAgIBATANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQGEwJSVTEP
+MA0GA1UECAwGTW9zY293MQ8wDQYDVQQKDAZZYW5kZXgxFTATBgNVBAMMDENBQGxv
+Y2FsaG9zdDAeFw0yMzA3MTMxMTUxMTdaFw0zMzA3MTAxMTUxMTdaMEMxCzAJBgNV
+BAYTAlJVMQ8wDQYDVQQIDAZNb3Njb3cxDzANBgNVBAoMBllhbmRleDESMBAGA1UE
+AwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1ZrD
++kcfmsi52zMZ7cJxWl8pMI1kX1uQ6KkOJRVQVuweJuhp35rJGK0vHkWvrpihO+Ue
+h1jncReH8PaNdejt3gPsYJksUeRxCaZ8O+awtyOq6TK8L9pDzbQyXme5k2Ea/+yl
+XSHrGGzZUuHaZRzBigyQXzb04ZdQMzqGGKUz52ed5elZnOoAMUL7/wZ7O8n5dlXh
+DwxEQ4SQ3a39SLpPWv3hWXiIDrlR5xhkqQjZnb0WEVlSwN8lpyjA6XwCs/IX7PVr
+khoBKq7664rsR3B7qhGA5cmSy4+KrSoYKYHGCvl6OakO7fn4FcJwHHFNQzMq8oa5
+i5LbYsaCy6IgJt07nBeNXxaEY+4WIImHfWR0yvQgqtrFQZuIQ4N7YeZpLvTtoO3I
+UJyjMsih5GX4LPY0k9nE8rd437lV3PdiBYAktkwmgCTY9uJFhCBPFsMhE30nL79t
+X0gm5FugW1aZ3CFvgQywkcYQIjBjJQ42zcpqnLjj4lIwfDO5YEAiAOPqQ3VV7FKm
+bSWpeXyRAL/uqkBVCDKTsKJkEjd8gc30honRIKNHQkr/RoV1M1W449uRkvfVfbfg
+kdcRcYMIynTwZMSiKDoxXkQyrcyY6qVjYQNUKwRvtN2m6aGftYhfgo4j01UHTrK7
+BHRHKjW5LFXgNJUXnycmO69B+w1eTRgnVE7ws6sCAwEAAaNNMEswCQYDVR0TBAIw
+ADAdBgNVHQ4EFgQUNvB1ghYixkG53VZoSvUPnCCN7hwwHwYDVR0jBBgwFoAUW5Z1
+bHNDpMX/IfexCeWTcQSvVWMwDQYJKoZIhvcNAQELBQADggIBAHW/+xxign61axqT
+hO338rJGuidhXzraCQtJT/J6OwUaI+P6C71wUoBKEk3SrIVNG1Q8mrlsj93EzQuv
+M9OvBenIomF7A2ydaFL/6NJhZ8W6PmZwYF3VvInYJ2p7zqCMjqECg2C92NIC7+Z6
+fdmL+xoj3XKYypqA1x6xvSnCtXyXGRCxta3Es163wbqffq+4jjBUFOyUr9vk2N7X
+vy3/x8LWHIXffzNSJLtnXiznSNBSubmedac8JQ+XE9RgK+R0kUrj1lqnkSg+tPWD
+jP52kG84J9URV18BZfLFpcUiYloWXREfwNXRhMAQ6DyucupLW1Skl+Nf9K+41C3h
+f4mDn4Axn6toBzav9NLdFelGVAp4R3Yjoiv2LvpwYxGnMs/cPJMGm/NqoAbuyOMY
+ZKZPWvwsbxeaG8u9GRGvTSpawnGWJqIgxknKpQT5QztjNwtI9iT0/f4n3CPEGdAi
+6bHw0q+jiCKmXZMZPHyJ/tSJ74H7tdeWYYjhthJWnrAz4BziZ+bFBLkcYq95VV4L
+pOeMUr0PUi8fpSK06wIVTES9AWgcfuXL6i7AQ+hadEa3Ve1BGRsu0KOXW2XZZFeo
+3Pczm/o+jwMiLELcgrM5Ngy8dcCKr6v84F+fi9Y+C8+RZ7g37aLJM0kqbaoN8owL
+mP88f9xDGRAmesvuYlHo+57VTyzU
+-----END CERTIFICATE-----)foo";
+ TSslContext::Get()->UseCertificateChain(certChain);
+
+ auto privateKey = R"foo(-----BEGIN PRIVATE KEY-----
+MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDVmsP6Rx+ayLnb
+MxntwnFaXykwjWRfW5DoqQ4lFVBW7B4m6GnfmskYrS8eRa+umKE75R6HWOdxF4fw
+9o116O3eA+xgmSxR5HEJpnw75rC3I6rpMrwv2kPNtDJeZ7mTYRr/7KVdIesYbNlS
+4dplHMGKDJBfNvThl1AzOoYYpTPnZ53l6Vmc6gAxQvv/Bns7yfl2VeEPDERDhJDd
+rf1Iuk9a/eFZeIgOuVHnGGSpCNmdvRYRWVLA3yWnKMDpfAKz8hfs9WuSGgEqrvrr
+iuxHcHuqEYDlyZLLj4qtKhgpgcYK+Xo5qQ7t+fgVwnAccU1DMyryhrmLkttixoLL
+oiAm3TucF41fFoRj7hYgiYd9ZHTK9CCq2sVBm4hDg3th5mku9O2g7chQnKMyyKHk
+Zfgs9jST2cTyt3jfuVXc92IFgCS2TCaAJNj24kWEIE8WwyETfScvv21fSCbkW6Bb
+VpncIW+BDLCRxhAiMGMlDjbNymqcuOPiUjB8M7lgQCIA4+pDdVXsUqZtJal5fJEA
+v+6qQFUIMpOwomQSN3yBzfSGidEgo0dCSv9GhXUzVbjj25GS99V9t+CR1xFxgwjK
+dPBkxKIoOjFeRDKtzJjqpWNhA1QrBG+03abpoZ+1iF+CjiPTVQdOsrsEdEcqNbks
+VeA0lRefJyY7r0H7DV5NGCdUTvCzqwIDAQABAoICAEvCv0TLKiH/lK/y4XzrTMIF
+Y3oVhCawNubWYy5y71JNH+qj3z1QTIgEkOQ3Sjbuaq1wN9JAjaIWewBTqlvKOGfY
+02N1oHsRP6hxFLo4ObBTJcDdXlLIoujYQ08pke/8bpOcDxDHwXch0Djt40SenOSG
+TUSAHP3QacEpvjsKiSzHmwDbMY4Oju/p9q/+0AGmQuUeU5s/Og0KfUkq911uu0um
+JWHS9srmHu8Mv1MW0Px5/tQ7brb6zoOJ2FZXxiulr6e7aiJhN824T0XwuZojArGQ
+0LtvsbGiYUjG19gM772fu6Ks3B863Ct3kcT8yK8PfGmVsESZW1ee2fA4uheeuw+c
+/3D71+dnjvw0/LFQEWyiH9NhjvafALBGxtlbfTTVtw5bHVaoalYpEggKnJrRabPa
+HmdPkEAK0vJzBvTjPW1lEvlxUkol1SsYq8xpBkatieZzf4+SWk/ugP7rvoRVRbB1
+GmHc9CK9jCDTcN7ii5pTOpc2VOK5cvn+K5L4P7Qw4kK37pyO56jhVbTAavNxotSc
++mZa8OqZaK0jvD9sUPCSuY44x5X6WhXjILE+R3QXXXtGkyjlgJxGmfkYcu19GV4B
+ziU7hVCjqtNOQ51ywEtWUA93lchj3fD5ryo9dCv5a5Xco4O5E24VeGjW3MBqeBNW
+wNIUintgwVt20P1I+xZRAoIBAQD9UzjlhvrDgNZsaJu82+PoOEJM2pa5F+fDGSeq
+XTYCCeqioMUujJWDEkBEzfTvu4r7Pr7uExOlkyUXdSELfm+ABkt+eZguPB15Ua6V
+Y59Z4yrpqp0JIwpxXGxNOsSUnVwGg3OTlUGWYVyxyGHYQnSGJNe2/NhSlQZmvxx2
+PxqTH+g9dLn7FkQk/FNQI4l4LnphgGvNBVyXZoHcw16y86MAKLyrcZBmbHnuWMC6
+zjIu61uXXd8GU7IU8BWJbUQKvz0Y5susWf921U/7Qa8NZ2vFY4Q7oGlJVBVhMNmB
+WLL8/WUeWXu9HvL89QWpb31l5V5L+lF+GSq5ZQxFAjLEVl77AoIBAQDX3CwirHtf
+MI7z+zjwLDWgVG7RFVY4Pv5TWDWYHiQT9VPBU16K0+fVS1iXxYvoiGJkrj38Re8y
+rkTZ+qvKLmlDftqx7bImbzs01QJRbUH2gxZFMDQIF7uTSFynycywaQk+xpFEPTHl
+CXMIyGAMAsd0B2OKPAxZNoHTd0P10FYBIENwxadG9w7Ocm41J/d4Qx6wQsV10hGF
+0OgtIioplSDz1Ean/IT8XmwLK7tIiShJzZidE+0sY4depy3JdAHWv50uXMs3bIcu
+xvVBA/e/jf+HssfLjzzrYtwp2JBzSOTox/eCUJ/i9gatC5ox+ewjwOaFXV3bGEib
+mK+I3xiQ3B8RAoIBAE0Jc/IJHFU75vlMzp+eVy6VfUQV7WQYavifu7pJYlU4YsxW
+C+DeC9GySS0jXOtSoy9Io5OO5ZiiqNL7YbM3Hf1W7Lpni+nzihsMxgTUKO+S78fj
+hKH0sAZNTvoldwai3At3CjzFVQ7ASQofn/G+M+VfauJQ/hAPFcVFNQiYpCI9v8iA
+qNY8rTh6K3Pherq7l6fy/9V3XfMEz1UtbK0K/nTb7pRMktczAdmD0Ah/EC/Ijy/2
+8g3ggfVwFXyXZ+vEwHXEKggdzlx6/jmwfeWbn+CFJP9lBt+v3FiUHHEDYlshTBDw
+sXqP4OEgOjqOlxnXqNd+Ji4sxRtgKV0LEBk5EuUCggEAIlBbq79jdURQ1TQQXw2I
+EM6bNx1/MT3CTBlvm5je/1U2VTsdglAhQGTT1nyOuw5DJeIU9G9hkNrnEweoG2G5
+VgNqXHJ+qWFxNfrOfYcyvy8jcSgyfT7YkJcmM33+zeRElfgWy5Q2xEP2R2Ui74XZ
+kvZBuo3FIMFrbeQ9p2vQ4Cjyz5B8AOnxLpw+LLEHw9RXoolavloAcxc8cUBHF4kf
+TeNmv/mCYmPYJQZ0pRk4kFLgecfbIf1IXaGRw75vNGYNZHtXyp2z95mlDwrEbWzz
+O+0NmaxRcNGsUfKdM9ZYnTB8hfivEfMuKH/5qQwjn6Nggb7P1q5LjIB/FvDwBMcZ
+IQKCAQBfPoddROG6bTkzn7kL8rDwuk2H2sLh5njRKo3u29dVlwU93lmKLoP4blcf
+tAbXDVrlGpZbIo73baE57Rv5ehcV64oS/G63Lcw8nsqmBvs9982tpYPNX9EOvXV5
+1iK+hs8ZUUVCVkTOXsZb5M90VKQiHrnddO+08lE5YI/lzM5laqcytPmYcLkVWPPz
+qrpW/AReSwhvwVugcMFUgMXaDx/3SAY75B808wX1tizv76omWZAQ774FeGQGyP4C
+8f4t3LIV9h/q2Hj8geMjil9ZGogtWJ5uDspp7As5OyMF0ZMXTMwSnFsXB/L4YIk+
+rPl77gAcribJm3TzBVHm2m6jBGtb
+-----END PRIVATE KEY-----)foo";
+ TSslContext::Get()->UsePrivateKey(privateKey);
+ TSslContext::Get()->CheckPrivateKeyWithCertificate();
+ }
+
+ IBusServerPtr StartBusServer(IMessageHandlerPtr handler)
+ {
+ auto config = TBusServerConfig::CreateTcp(Port);
+ config->EncryptionMode = EEncryptionMode::Required;
+ config->UseKeyPairFromSslContext = true;
+ auto server = CreateBusServer(config);
+ server->Start(handler);
+ return server;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSslTest, RequiredAndRequiredEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Required;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Required;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_TRUE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, RequiredAndOptionalEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Required;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Optional;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_TRUE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, OptionalAndRequiredEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Optional;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Required;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_TRUE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, OptionalAndOptionalEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Optional;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Optional;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_FALSE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, DisabledAndDisabledEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Disabled;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Disabled;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_FALSE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, RequiredAndDisabledEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Required;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Disabled;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_FALSE(bus->GetReadyFuture().Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, DisabledAndRequiredEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Required;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Disabled;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_FALSE(bus->GetReadyFuture().Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, DisabledAndOptionalEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Disabled;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Optional;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_FALSE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, OptionalAndDisabledEncryptionMode)
+{
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Optional;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Disabled;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_FALSE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, CAVerificationModeFailure)
+{
+ // Reset ctx in order to unload possibly loaded CA.
+ TSslContext::Get()->Reset();
+ LoadKeyPairIntoCxt();
+
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Required;
+ serverConfig->VerificationMode = EVerificationMode::None;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Required;
+ clientConfig->VerificationMode = EVerificationMode::Ca;
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_FALSE(bus->GetReadyFuture().Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, CAVerificationModeSuccess)
+{
+ // Reset ctx in order to unload possibly loaded CA.
+ TSslContext::Get()->Reset();
+ LoadKeyPairIntoCxt();
+
+ // Load CA into ctx.
+ auto ca = R"foo(-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgIBATANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQGEwJSVTEP
+MA0GA1UECAwGTW9zY293MQ8wDQYDVQQKDAZZYW5kZXgxFTATBgNVBAMMDENBQGxv
+Y2FsaG9zdDAeFw0yMzA3MTMxMTUxMTFaFw0zMzA3MTAxMTUxMTFaMEYxCzAJBgNV
+BAYTAlJVMQ8wDQYDVQQIDAZNb3Njb3cxDzANBgNVBAoMBllhbmRleDEVMBMGA1UE
+AwwMQ0FAbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
+mxWRSfbYAJcBgGhYagnQMJOiW60B82Ok94n8muIaa2gRo0ZHKmtY7CMiFIIN0GhI
+Blw+G6HPN8SPYK0+bTlAIHlb0u6/ty2AVyveiH31p7Ld/Ib5S/MSKuMiGt5S/Ci9
+mSdRAmD46twIKfm9bY/8SZmNsTCJIrKnJaTbjTnbz9O3FVAYjiuc8yNGb1LnZNA6
+B4ZrF3fkYr9kn4nKSOd6mypPFIOZAxKGQ95X9nCblajEMAPzHfr+1EArnM+PauAO
+cMcxfKT+OlMPGR4ZtZpl90qX6ZVZqD9zd8gp2I/2SM1vVS7AT96t89T7Uag8OjH2
+c8jMCD2z1fk1oDD4K53pGkBucTwDolCvxIbN77gcwkutdor/dbHLqIvGV/M4Z/iA
+GzqCD6pCpi5gT8SXn2IQrlvSdF01k9YZS093Y8bIQm3dCo1lKDaz6oHytk9Ro+fu
+b+dLOhSjopNfa/1Thw6fgta/mRsdgubWI/IHn+IyMpW65vGbnrMD4oQl1AanRJj5
+iBS73ZXIs/Y9LuMtSNk/I9u1o2fc+Sg0zb1AchC70h8M3sAYaGbqg1churOaDuTO
+Yom5W+bAQO8uLUvmlDcpqxMBYqeE8VJGJWRLtQnhM18iYAa03w7m6uMzrlkoX1Q7
+AHnX4899WSUcz/BJc5JBAtHQXCzEzyudT+8xw5MwqRkCAwEAAaNTMFEwHQYDVR0O
+BBYEFFuWdWxzQ6TF/yH3sQnlk3EEr1VjMB8GA1UdIwQYMBaAFFuWdWxzQ6TF/yH3
+sQnlk3EEr1VjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAHqS
+W4FIjH/YKkcj/wuh7sQQTIBAx+LsjosyHb9K/1QbgLNVpxawl/YlOiArfVmgTGud
+9B6xdVacxqI6saMqHNPPgYm7OPnozSnprRKh9yGb2TS9j+2G7hCQLnM+xYroP8XE
+8fL9tyq14lHb/BFYnPsFFxqeA6lXzzm73hlm/hH58CWpL/3eR/TDjC+oiEBBV4VF
+Dm4X3L73MFcniDKkCb7Mw3wdi6zqx231F4+9Cqgq+RqAucnmLXTG7yR7wOKP2u8E
+Ye1Jrt3nnMlD1tqiXyRJywPSK9mMiBTGbzGLLiTcvecBIHG8oRuvn1DnkZa9L/48
++1aTC8QhbRslaeDyXj5IY/7scW7ZkEw18qYCT+9sI4/LngS4U8+taKqLf5S8A5sR
+O8OCP0nMk/l5f84cDwe1dp4GXVQdkbnfT1sd93BLww7Szbw2iMt9sLMmyY8qIe7a
+Ss3OEfP6eyNCBu6KtL8oufdj1AqAQdmYYTlGwgaFZTAomDiJU662EGSt3uXK7V32
+EzgJbpxSSjWh5JpCDA/jnqzlkFkSaw92/HZwO5l1lCdqK+F1UWYQyU0NveOaSEuX
+344PIi8m1YWwUfukUB97D+C6B4y5vgdkC7OYLHb4W4+N0ZvYtbDDaBeTpn70CiI7
+JFWcF3ghP7uPmbONWLiTFwxsSJHT0svVQZgq1aZz
+-----END CERTIFICATE-----)foo";
+ TSslContext::Get()->UseCA(ca);
+
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Required;
+ serverConfig->VerificationMode = EVerificationMode::None;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Required;
+ clientConfig->VerificationMode = EVerificationMode::Ca;
+ clientConfig->CAFile = "unused if CA has already been loaded";
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_TRUE(bus->IsEncrypted());
+
+ for (int i = 0; i < 2; ++i) {
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ Cerr << sendFuture.Get().GetMessage() << Endl;
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+ }
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TSslTest, FullVerificationMode)
+{
+ // Reset ctx in order to unload possibly loaded CA.
+ TSslContext::Get()->Reset();
+ LoadKeyPairIntoCxt();
+
+ // Load CA into ctx.
+ auto ca = R"foo(-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgIBATANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQGEwJSVTEP
+MA0GA1UECAwGTW9zY293MQ8wDQYDVQQKDAZZYW5kZXgxFTATBgNVBAMMDENBQGxv
+Y2FsaG9zdDAeFw0yMzA3MTMxMTUxMTFaFw0zMzA3MTAxMTUxMTFaMEYxCzAJBgNV
+BAYTAlJVMQ8wDQYDVQQIDAZNb3Njb3cxDzANBgNVBAoMBllhbmRleDEVMBMGA1UE
+AwwMQ0FAbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
+mxWRSfbYAJcBgGhYagnQMJOiW60B82Ok94n8muIaa2gRo0ZHKmtY7CMiFIIN0GhI
+Blw+G6HPN8SPYK0+bTlAIHlb0u6/ty2AVyveiH31p7Ld/Ib5S/MSKuMiGt5S/Ci9
+mSdRAmD46twIKfm9bY/8SZmNsTCJIrKnJaTbjTnbz9O3FVAYjiuc8yNGb1LnZNA6
+B4ZrF3fkYr9kn4nKSOd6mypPFIOZAxKGQ95X9nCblajEMAPzHfr+1EArnM+PauAO
+cMcxfKT+OlMPGR4ZtZpl90qX6ZVZqD9zd8gp2I/2SM1vVS7AT96t89T7Uag8OjH2
+c8jMCD2z1fk1oDD4K53pGkBucTwDolCvxIbN77gcwkutdor/dbHLqIvGV/M4Z/iA
+GzqCD6pCpi5gT8SXn2IQrlvSdF01k9YZS093Y8bIQm3dCo1lKDaz6oHytk9Ro+fu
+b+dLOhSjopNfa/1Thw6fgta/mRsdgubWI/IHn+IyMpW65vGbnrMD4oQl1AanRJj5
+iBS73ZXIs/Y9LuMtSNk/I9u1o2fc+Sg0zb1AchC70h8M3sAYaGbqg1churOaDuTO
+Yom5W+bAQO8uLUvmlDcpqxMBYqeE8VJGJWRLtQnhM18iYAa03w7m6uMzrlkoX1Q7
+AHnX4899WSUcz/BJc5JBAtHQXCzEzyudT+8xw5MwqRkCAwEAAaNTMFEwHQYDVR0O
+BBYEFFuWdWxzQ6TF/yH3sQnlk3EEr1VjMB8GA1UdIwQYMBaAFFuWdWxzQ6TF/yH3
+sQnlk3EEr1VjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAHqS
+W4FIjH/YKkcj/wuh7sQQTIBAx+LsjosyHb9K/1QbgLNVpxawl/YlOiArfVmgTGud
+9B6xdVacxqI6saMqHNPPgYm7OPnozSnprRKh9yGb2TS9j+2G7hCQLnM+xYroP8XE
+8fL9tyq14lHb/BFYnPsFFxqeA6lXzzm73hlm/hH58CWpL/3eR/TDjC+oiEBBV4VF
+Dm4X3L73MFcniDKkCb7Mw3wdi6zqx231F4+9Cqgq+RqAucnmLXTG7yR7wOKP2u8E
+Ye1Jrt3nnMlD1tqiXyRJywPSK9mMiBTGbzGLLiTcvecBIHG8oRuvn1DnkZa9L/48
++1aTC8QhbRslaeDyXj5IY/7scW7ZkEw18qYCT+9sI4/LngS4U8+taKqLf5S8A5sR
+O8OCP0nMk/l5f84cDwe1dp4GXVQdkbnfT1sd93BLww7Szbw2iMt9sLMmyY8qIe7a
+Ss3OEfP6eyNCBu6KtL8oufdj1AqAQdmYYTlGwgaFZTAomDiJU662EGSt3uXK7V32
+EzgJbpxSSjWh5JpCDA/jnqzlkFkSaw92/HZwO5l1lCdqK+F1UWYQyU0NveOaSEuX
+344PIi8m1YWwUfukUB97D+C6B4y5vgdkC7OYLHb4W4+N0ZvYtbDDaBeTpn70CiI7
+JFWcF3ghP7uPmbONWLiTFwxsSJHT0svVQZgq1aZz
+-----END CERTIFICATE-----)foo";
+ TSslContext::Get()->UseCA(ca);
+
+ auto serverConfig = TBusServerConfig::CreateTcp(Port);
+ serverConfig->UseKeyPairFromSslContext = true;
+ serverConfig->EncryptionMode = EEncryptionMode::Required;
+ serverConfig->VerificationMode = EVerificationMode::None;
+ auto server = CreateBusServer(serverConfig);
+ server->Start(New<TEmptyBusHandler>());
+
+ auto clientConfig = TBusClientConfig::CreateTcp(Address);
+ clientConfig->EncryptionMode = EEncryptionMode::Required;
+ clientConfig->VerificationMode = EVerificationMode::Full;
+ clientConfig->CAFile = "unused if CA has already been loaded";
+ auto client = CreateBusClient(clientConfig);
+
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ // This test should pass since key pair is issued for CN=localhost.
+ EXPECT_TRUE(bus->GetReadyFuture().Get().IsOK());
+ EXPECT_TRUE(bus->IsEncrypted());
+
+ auto message = CreateMessage(1);
+ auto sendFuture = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(sendFuture.Get().IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..40dfc858e3
--- /dev/null
+++ b/yt/yt/core/bus/unittests/ya.make
@@ -0,0 +1,42 @@
+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
+ ssl_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..099cc8aa52
--- /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;
+}
+
+bool 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 false;
+ }
+
+ // 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..b625c43bd6
--- /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;
+
+ bool BeginExecute(TEnqueuedAction* action);
+ void EndExecute(TEnqueuedAction* action);
+
+ void Reconfigure(std::vector<double> weights);
+
+private:
+ constexpr static i64 UnitWeight = 1'000;
+
+ 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..fed943be93
--- /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 BeginExecuteImpl(Queue_->BeginExecute(&CurrentAction_), &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..68da649486
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_thread_pool.cpp
@@ -0,0 +1,591 @@
+#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;
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver /*waitTimeObserver*/) override
+ { }
+
+ ~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();
+ }
+ }
+
+ bool 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 false;
+ }
+
+ ++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 true;
+ }
+
+ 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 BeginExecuteImpl(Queue_->BeginExecute(&CurrentAction_, Index_), &CurrentAction_);
+ }
+
+ 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..76144fd791
--- /dev/null
+++ b/yt/yt/core/concurrency/fiber_scheduler_thread.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "private.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..5b77a048df
--- /dev/null
+++ b/yt/yt/core/concurrency/invoker_queue.cpp
@@ -0,0 +1,613 @@
+#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;
+ }
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver) override
+ {
+ if (auto queue = Queue_.Lock()) {
+ queue->RegisterWaitTimeObserver(waitTimeObserver);
+ }
+ }
+
+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>
+bool TInvokerQueue<TQueueImpl>::BeginExecute(TEnqueuedAction* action, typename TQueueImpl::TConsumerToken* token)
+{
+ YT_ASSERT(action && action->Finished);
+
+ if (!QueueImpl_.TryDequeue(action, token)) {
+ return false;
+ }
+
+ auto cpuInstant = GetCpuInstant();
+
+ action->StartedAt = cpuInstant;
+
+ auto waitTime = CpuDurationToDuration(action->StartedAt - action->EnqueuedAt);
+
+ if (IsWaitTimeObserverSet_.load()) {
+ WaitTimeObserver_(waitTime);
+ }
+
+ 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 true;
+}
+
+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>
+void TInvokerQueue<TQueueImpl>::RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver)
+{
+ WaitTimeObserver_ = waitTimeObserver;
+ auto alreadyInitialized = IsWaitTimeObserverSet_.exchange(true);
+
+ // Multiple observers are forbidden.
+ YT_VERIFY(!alreadyInitialized);
+}
+
+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..556a2ea888
--- /dev/null
+++ b/yt/yt/core/concurrency/invoker_queue.h
@@ -0,0 +1,219 @@
+#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();
+
+ bool 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);
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver) override;
+
+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_;
+
+ std::atomic<bool> IsWaitTimeObserverSet_;
+ TWaitTimeObserver WaitTimeObserver_;
+
+ 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..56d0bd633c
--- /dev/null
+++ b/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp
@@ -0,0 +1,1194 @@
+#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;
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver /*waitTimeObserver*/) override
+ { }
+
+private:
+ const TTwoLevelFairShareQueuePtr Parent_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TBucket)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ERequest,
+ (None)
+ (EndExecute)
+ (FetchNext)
+);
+
+class TTwoLevelFairShareQueue
+ : public TRefCounted
+ , protected TNotifyManager
+{
+public:
+ using TWaitTimeObserver = ITwoLevelFairShareThreadPool::TWaitTimeObserver;
+
+ 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();
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver)
+ {
+ WaitTimeObserver_ = waitTimeObserver;
+ auto alreadyInitialized = IsWaitTimeObserverSet_.exchange(true);
+
+ // Multiple observers are forbidden.
+ YT_VERIFY(!alreadyInitialized);
+ }
+
+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;
+
+ std::atomic<bool> IsWaitTimeObserverSet_;
+ TWaitTimeObserver WaitTimeObserver_;
+
+ 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);
+ ReportWaitTime(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);
+ }
+
+ void ReportWaitTime(TDuration waitTime)
+ {
+ if (IsWaitTimeObserverSet_.load()) {
+ WaitTimeObserver_(waitTime);
+ }
+ }
+};
+
+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();
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver) override
+ {
+ Queue_->RegisterWaitTimeObserver(waitTimeObserver);
+ }
+
+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..fcbcefe605
--- /dev/null
+++ b/yt/yt/core/concurrency/private.h
@@ -0,0 +1,68 @@
+#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(TSchedulerThreadBase)
+
+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..7d89333427
--- /dev/null
+++ b/yt/yt/core/concurrency/scheduler_thread.cpp
@@ -0,0 +1,124 @@
+#include "scheduler_thread.h"
+
+#include "private.h"
+#include "invoker_queue.h"
+
+#include <yt/yt/core/misc/hazard_ptr.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;
+ }
+
+ constexpr auto WaitTimeout = TDuration::Seconds(1);
+ if (!CallbackEventCount_->Wait(cookie, WaitTimeout)) {
+ MaybeRunMaintenance(GetCpuInstant());
+ }
+
+ EndExecute();
+ }
+}
+
+TClosure TSchedulerThread::BeginExecuteImpl(bool dequeued, TEnqueuedAction* action)
+{
+ if (!dequeued) {
+ return {};
+ }
+ MaybeRunMaintenance(action->StartedAt);
+ return std::move(action->Callback);
+}
+
+void TSchedulerThread::MaybeRunMaintenance(TCpuInstant now)
+{
+ // 1B clock cycles between maintenance iterations.
+ constexpr i64 MaintenancePeriod = 1'000'000'000;
+ if (now > LastMaintenanceInstant_ + MaintenancePeriod) {
+ RunMaintenance();
+ LastMaintenanceInstant_ = now;
+ }
+}
+
+void TSchedulerThread::RunMaintenance()
+{
+ ReclaimHazardPointers(/*flush*/ false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //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..7cf4ee151b
--- /dev/null
+++ b/yt/yt/core/concurrency/scheduler_thread.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include "fiber_scheduler_thread.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+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;
+
+ TClosure BeginExecuteImpl(bool dequeued, TEnqueuedAction* action);
+
+private:
+ TCpuInstant LastMaintenanceInstant_ = 0;
+
+ void MaybeRunMaintenance(TCpuInstant now);
+ void RunMaintenance();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //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..0b66712d25
--- /dev/null
+++ b/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp
@@ -0,0 +1,155 @@
+#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 BeginExecuteImpl(Queue_->BeginExecute(&CurrentAction_, &Token_), &CurrentAction_);
+}
+
+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();
+ }
+
+ if (!Queue_->BeginExecute(&CurrentAction_, &Token_)) {
+ return {};
+ }
+ return std::move(CurrentAction_.Callback);
+}
+
+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..b2975b4baa
--- /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)
+ { }
+
+ template <class TIsStoppingPredicate>
+ bool OnExecute(TEnqueuedAction* action, bool fetchNext, TIsStoppingPredicate isStopping)
+ {
+ while (true) {
+ int activeThreadDelta = !action->Finished ? -1 : 0;
+ TMpmcInvokerQueue::EndExecute(action);
+
+ auto cookie = GetEventCount()->PrepareWait();
+ auto minEnqueuedAt = ResetMinEnqueuedAt();
+
+ bool result = false;
+ if (fetchNext && TMpmcInvokerQueue::BeginExecute(action)) {
+ YT_ASSERT(action->EnqueuedAt > 0);
+ minEnqueuedAt = action->EnqueuedAt;
+ activeThreadDelta += 1;
+ result = true;
+ }
+
+ YT_VERIFY(activeThreadDelta <= 1 && activeThreadDelta >= -1);
+ if (activeThreadDelta != 0) {
+ auto activeThreads = ActiveThreads_.fetch_add(activeThreadDelta) + activeThreadDelta;
+ YT_VERIFY(activeThreads >= 0 && activeThreads <= TThreadPoolBase::MaxThreadCount);
+ }
+
+ if (result || isStopping()) {
+ CancelWait();
+
+ NotifyAfterFetch(GetCpuInstant(), minEnqueuedAt);
+ return result;
+ }
+
+ 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_;
+
+ bool dequeued = Queue_->OnExecute(&CurrentAction_, fetchNext, [&] {
+ return TSchedulerThread::IsStopping();
+ });
+ return BeginExecuteImpl(dequeued, &CurrentAction_);
+ }
+
+ 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..d554844235
--- /dev/null
+++ b/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp
@@ -0,0 +1,742 @@
+#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;
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver /*waitTimeObserver*/) override
+ { }
+
+ ~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:
+ using TWaitTimeObserver = ITwoLevelFairShareThreadPool::TWaitTimeObserver;
+
+ 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();
+ }
+ }
+ }
+ }
+
+ bool BeginExecute(TEnqueuedAction* action, int index)
+ {
+ auto& threadState = ThreadStates_[index];
+
+ YT_ASSERT(!threadState.Bucket);
+ YT_ASSERT(action && action->Finished);
+
+ auto currentInstant = GetCpuInstant();
+
+ TBucketPtr bucket;
+ TWaitTimeObserver waitTimeObserver;
+
+ {
+ auto guard = Guard(SpinLock_);
+ bucket = GetStarvingBucket(action);
+ waitTimeObserver = WaitTimeObserver_;
+
+ if (!bucket) {
+ return false;
+ }
+
+ ++bucket->CurrentExecutions;
+
+ threadState.Bucket = bucket;
+ threadState.AccountedAt = currentInstant;
+
+ action->StartedAt = currentInstant;
+ bucket->WaitTime = action->StartedAt - action->EnqueuedAt;
+ }
+
+ if (waitTimeObserver) {
+ waitTimeObserver(CpuDurationToDuration(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 true;
+ }
+
+ 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);
+ }
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver)
+ {
+ auto guard = Guard(SpinLock_);
+ WaitTimeObserver_ = waitTimeObserver;
+ }
+
+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_;
+
+ ITwoLevelFairShareThreadPool::TWaitTimeObserver WaitTimeObserver_;
+
+
+ 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 BeginExecuteImpl(Queue_->BeginExecute(&CurrentAction_, Index_), &CurrentAction_);
+ }
+
+ 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();
+ }
+
+ void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver) override
+ {
+ Queue_->RegisterWaitTimeObserver(std::move(waitTimeObserver));
+ }
+
+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..fcfdd480ff
--- /dev/null
+++ b/yt/yt/core/concurrency/two_level_fair_share_thread_pool.h
@@ -0,0 +1,46 @@
+#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;
+
+ using TWaitTimeObserver = std::function<void(TDuration)>;
+ virtual void RegisterWaitTimeObserver(TWaitTimeObserver waitTimeObserver) = 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..c5309d1175
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/periodic_ut.cpp
@@ -0,0 +1,231 @@
+#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();
+}
+
+// TODO(achulkov2): Add simple out of band execution test.
+
+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..291f2b8d49
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/scheduler_ut.cpp
@@ -0,0 +1,1621 @@
+#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>>
+{ };
+
+bool ApproximatelyEqual(TDuration lhs, TDuration rhs, TDuration eps = TDuration::MilliSeconds(1))
+{
+ return (lhs < rhs ? rhs - lhs : lhs - rhs) < eps;
+}
+
+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 startInstant = GetCpuInstant();
+
+ auto poolId = id % numPools;
+ auto initialShift = getShift(id);
+
+ Sleep(initialShift);
+
+ {
+ auto guard = Guard(lock);
+ auto elapsedTime = CpuDurationToDuration(GetCpuInstant() - startInstant);
+ pools[poolId] += elapsedTime;
+ progresses[id] += elapsedTime;
+ }
+
+ Yield();
+
+ while (progresses[id] < work + initialShift) {
+ auto startInstant = GetCpuInstant();
+
+ {
+ 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];
+ }
+ }
+
+ YT_LOG_DEBUG("Pools time: %v", pools);
+ YT_LOG_DEBUG("Progresses time: %v", progresses);
+
+ EXPECT_TRUE(ApproximatelyEqual(pools[poolId], minPool));
+
+ auto minProgress = TDuration::Max();
+ for (size_t index = poolId; index < numWorkers; index += numPools) {
+ if (progresses[index] < minProgress && progresses[index] < work + getShift(index)) {
+ minProgress = progresses[index];
+ }
+ }
+
+ EXPECT_TRUE(ApproximatelyEqual(progresses[id], minProgress));
+ }
+ }
+
+ 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 startInstant = GetCpuInstant();
+
+ auto initialShift = getShift(id);
+
+ Sleep(initialShift);
+
+ {
+ auto guard = Guard(lock);
+ auto elapsedTime = CpuDurationToDuration(GetCpuInstant() - startInstant);
+ progresses[id] += elapsedTime;
+ }
+
+ Yield();
+
+ while (progresses[id] < work + initialShift) {
+ auto startInstant = GetCpuInstant();
+
+ {
+ auto guard = Guard(lock);
+
+ if (numThreads == 1) {
+ YT_LOG_DEBUG("Progresses time: %v", progresses);
+
+ auto minProgress = TDuration::Max();
+ for (size_t id = 0; id < numWorkers; ++id) {
+ if (progresses[id] < minProgress && progresses[id] < work + getShift(id)) {
+ minProgress = progresses[id];
+ }
+ }
+
+ EXPECT_TRUE(ApproximatelyEqual(progresses[id], minProgress));
+ }
+ }
+
+ 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..1e87f3ac4c
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/ya.make
@@ -0,0 +1,62 @@
+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_TEST_FILES()
+
+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/CMakeLists.darwin-x86_64.txt b/yt/yt/core/crypto/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..926c90ce8d
--- /dev/null
+++ b/yt/yt/core/crypto/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-core-crypto)
+target_compile_options(yt-core-crypto PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-crypto PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ OpenSSL::OpenSSL
+ cpp-openssl-io
+)
+target_sources(yt-core-crypto PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/crypto.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/tls.cpp
+)
diff --git a/yt/yt/core/crypto/CMakeLists.linux-aarch64.txt b/yt/yt/core/crypto/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..0fceec4097
--- /dev/null
+++ b/yt/yt/core/crypto/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-core-crypto)
+target_compile_options(yt-core-crypto PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-crypto PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ OpenSSL::OpenSSL
+ cpp-openssl-io
+)
+target_sources(yt-core-crypto PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/crypto.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/tls.cpp
+)
diff --git a/yt/yt/core/crypto/CMakeLists.linux-x86_64.txt b/yt/yt/core/crypto/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..0fceec4097
--- /dev/null
+++ b/yt/yt/core/crypto/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-core-crypto)
+target_compile_options(yt-core-crypto PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-crypto PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ OpenSSL::OpenSSL
+ cpp-openssl-io
+)
+target_sources(yt-core-crypto PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/crypto.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/tls.cpp
+)
diff --git a/yt/yt/core/crypto/CMakeLists.txt b/yt/yt/core/crypto/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/core/crypto/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/core/crypto/CMakeLists.windows-x86_64.txt b/yt/yt/core/crypto/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..fbc748e5cb
--- /dev/null
+++ b/yt/yt/core/crypto/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+find_package(OpenSSL REQUIRED)
+
+add_library(yt-core-crypto)
+target_link_libraries(yt-core-crypto PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ OpenSSL::OpenSSL
+ cpp-openssl-io
+)
+target_sources(yt-core-crypto PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/crypto.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/crypto/tls.cpp
+)
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..035ec059e9
--- /dev/null
+++ b/yt/yt/core/dns/ares_dns_resolver.cpp
@@ -0,0 +1,524 @@
+#include "dns_resolver.h"
+
+#include "config.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:
+ explicit TAresDnsResolver(TAresDnsResolverConfigPtr config)
+ : Config_(std::move(config))
+ , 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>(Config_->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>(Config_->MaxResolveTimeout.MilliSeconds());
+ mask |= ARES_OPT_MAXTIMEOUTMS;
+ #endif
+
+ #ifdef ARES_OPT_JITTER
+ if (Config_->Jitter) {
+ Options_.jitter = llround(*Config_->Jitter * 1000.0);
+ Options_.jitter_rand_seed = TGuid::Create().Parts32[0];
+ mask |= ARES_OPT_JITTER;
+ }
+ #endif
+
+ Options_.tries = Config_->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"));
+ }),
+ Config_->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 TAresDnsResolverConfigPtr Config_;
+
+ 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 > Config_->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(TAresDnsResolverConfigPtr config)
+{
+ return New<TAresDnsResolver>(std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..d20181f851
--- /dev/null
+++ b/yt/yt/core/dns/ares_dns_resolver.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDnsResolverPtr CreateAresDnsResolver(TAresDnsResolverConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
+
diff --git a/yt/yt/core/dns/config.cpp b/yt/yt/core/dns/config.cpp
new file mode 100644
index 0000000000..302cb67eb1
--- /dev/null
+++ b/yt/yt/core/dns/config.cpp
@@ -0,0 +1,25 @@
+#include "config.h"
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAresDnsResolverConfig::Register(TRegistrar registrar)
+{
+ 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);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
diff --git a/yt/yt/core/dns/config.h b/yt/yt/core/dns/config.h
new file mode 100644
index 0000000000..b6d7c6e5cc
--- /dev/null
+++ b/yt/yt/core/dns/config.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAresDnsResolverConfig
+ : public virtual NYTree::TYsonStruct
+{
+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;
+ std::optional<double> Jitter;
+ TDuration WarningTimeout;
+ //! Used to check that bootstrap is being initialized from a correct container.
+ std::optional<TString> ExpectedLocalHostName;
+
+ REGISTER_YSON_STRUCT(TAresDnsResolverConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TAresDnsResolverConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..175c6ab62f
--- /dev/null
+++ b/yt/yt/core/dns/public.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDnsResolveOptions;
+
+DECLARE_REFCOUNTED_CLASS(TAresDnsResolverConfig)
+
+DECLARE_REFCOUNTED_STRUCT(IDnsResolver)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
diff --git a/yt/yt/core/http/CMakeLists.darwin-x86_64.txt b/yt/yt/core/http/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..8b5195db72
--- /dev/null
+++ b/yt/yt/core/http/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,29 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-http)
+target_compile_options(yt-core-http PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-http PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ contrib-restricted-http-parser
+)
+target_sources(yt-core-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_reuse_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/helpers.cpp
+)
diff --git a/yt/yt/core/http/CMakeLists.linux-aarch64.txt b/yt/yt/core/http/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..96274c67f3
--- /dev/null
+++ b/yt/yt/core/http/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,30 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-http)
+target_compile_options(yt-core-http PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-http PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ contrib-restricted-http-parser
+)
+target_sources(yt-core-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_reuse_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/helpers.cpp
+)
diff --git a/yt/yt/core/http/CMakeLists.linux-x86_64.txt b/yt/yt/core/http/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..96274c67f3
--- /dev/null
+++ b/yt/yt/core/http/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,30 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-http)
+target_compile_options(yt-core-http PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-http PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ contrib-restricted-http-parser
+)
+target_sources(yt-core-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_reuse_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/helpers.cpp
+)
diff --git a/yt/yt/core/http/CMakeLists.txt b/yt/yt/core/http/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/core/http/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/core/http/CMakeLists.windows-x86_64.txt b/yt/yt/core/http/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..47398129ec
--- /dev/null
+++ b/yt/yt/core/http/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-http)
+target_link_libraries(yt-core-http PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ contrib-restricted-http-parser
+)
+target_sources(yt-core-http PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_pool.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/connection_reuse_helpers.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/http.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/server.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/stream.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/http/helpers.cpp
+)
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/CMakeLists.darwin-x86_64.txt b/yt/yt/core/https/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..a39f8e2881
--- /dev/null
+++ b/yt/yt/core/https/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-https)
+target_compile_options(yt-core-https PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-https PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ yt-core-http
+ yt-core-crypto
+ cpp-http-io
+)
+target_sources(yt-core-https PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/server.cpp
+)
diff --git a/yt/yt/core/https/CMakeLists.linux-aarch64.txt b/yt/yt/core/https/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..42080cc6aa
--- /dev/null
+++ b/yt/yt/core/https/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-https)
+target_compile_options(yt-core-https PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-https PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ yt-core-http
+ yt-core-crypto
+ cpp-http-io
+)
+target_sources(yt-core-https PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/server.cpp
+)
diff --git a/yt/yt/core/https/CMakeLists.linux-x86_64.txt b/yt/yt/core/https/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..42080cc6aa
--- /dev/null
+++ b/yt/yt/core/https/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,27 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-https)
+target_compile_options(yt-core-https PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-core-https PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ yt-core-http
+ yt-core-crypto
+ cpp-http-io
+)
+target_sources(yt-core-https PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/server.cpp
+)
diff --git a/yt/yt/core/https/CMakeLists.txt b/yt/yt/core/https/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/core/https/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/core/https/CMakeLists.windows-x86_64.txt b/yt/yt/core/https/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..27f0c0acae
--- /dev/null
+++ b/yt/yt/core/https/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-core-https)
+target_link_libraries(yt-core-https PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-core
+ yt-core-http
+ yt-core-crypto
+ cpp-http-io
+)
+target_sources(yt-core-https PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/config.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/client.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/https/server.cpp
+)
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..babeed087e
--- /dev/null
+++ b/yt/yt/core/logging/config.cpp
@@ -0,0 +1,508 @@
+#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 TRotationPolicyConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_total_size_to_keep", &TThis::MaxTotalSizeToKeep)
+ .Default(std::numeric_limits<i64>::max())
+ .GreaterThan(0);
+ registrar.Parameter("max_segment_count_to_keep", &TThis::MaxSegmentCountToKeep)
+ .Default(std::numeric_limits<i64>::max())
+ .GreaterThan(0);
+ registrar.Parameter("max_segment_size", &TThis::MaxSegmentSize)
+ .Default(std::nullopt)
+ .GreaterThan(0);
+ registrar.Parameter("rotation_period", &TThis::RotationPeriod)
+ .Default(std::nullopt)
+ .GreaterThan(TDuration::Zero());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFileLogWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("file_name", &TThis::FileName);
+ registrar.Parameter("use_timestamp_suffix", &TThis::UseTimestampSuffix)
+ .Default(false);
+ 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.Parameter("rotation_policy", &TThis::RotationPolicy)
+ .DefaultNew();
+
+ 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("rotation_check_period", &TThis::RotationCheckPeriod)
+ .Default(TDuration::Seconds(5))
+ .GreaterThanOrEqual(TDuration::Seconds(1));
+ 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..a65dcf6996
--- /dev/null
+++ b/yt/yt/core/logging/config.h
@@ -0,0 +1,238 @@
+#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 TRotationPolicyConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ //! Upper limit on the total size of rotated log files.
+ i64 MaxTotalSizeToKeep;
+ //! Upper limit on the number of rotated log files.
+ i64 MaxSegmentCountToKeep;
+ //! Limit on the size of current log file, which triggers rotation.
+ std::optional<i64> MaxSegmentSize;
+ //! Period of regular log file rotation.
+ std::optional<TDuration> RotationPeriod;
+
+ REGISTER_YSON_STRUCT(TRotationPolicyConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRotationPolicyConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileLogWriterConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ static constexpr const TStringBuf Type = "file";
+
+ TString FileName;
+ bool UseTimestampSuffix;
+ bool EnableCompression;
+ ECompressionMethod CompressionMethod;
+ int CompressionLevel;
+
+ TRotationPolicyConfigPtr RotationPolicy;
+
+ 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;
+ TDuration RotationCheckPeriod;
+
+ 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..191d39b7f9
--- /dev/null
+++ b/yt/yt/core/logging/file_log_writer.cpp
@@ -0,0 +1,354 @@
+#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)
+ , DirectoryName_(NFS::GetDirectoryName(Config_->FileName))
+ , FileNamePrefix_(NFS::GetFileName(Config_->FileName))
+ , LastRotationTimestamp_(TInstant::Now())
+ {
+ Open();
+ }
+
+ void Reload() override
+ {
+ Close();
+ Open();
+ }
+
+ const TString& GetFileName() const override
+ {
+ return FileName_;
+ }
+
+ void MaybeRotate() override
+ {
+ const auto& rotationPolicy = Config_->RotationPolicy;
+ auto now = TInstant::Now();
+ if ((!rotationPolicy->RotationPeriod || LastRotationTimestamp_ + *rotationPolicy->RotationPeriod > now) &&
+ ((!rotationPolicy->MaxSegmentSize || File_->GetLength() < *rotationPolicy->MaxSegmentSize)))
+ {
+ return;
+ }
+
+ Close();
+ Rotate();
+ Open();
+ }
+
+ void CheckSpace(i64 minSpace) override
+ {
+ try {
+ 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_;
+
+ const TString DirectoryName_;
+ const TString FileNamePrefix_;
+ TString FileName_;
+
+ std::atomic<bool> Disabled_ = false;
+ TInstant LastRotationTimestamp_;
+
+ std::unique_ptr<TFile> File_;
+ IStreamLogOutputPtr OutputStream_;
+
+
+ void Open()
+ {
+ Disabled_ = false;
+ try {
+ LastRotationTimestamp_ = TInstant::Now();
+ NFS::MakeDirRecursive(DirectoryName_);
+
+ TFlags<EOpenModeFlag> openMode;
+ if (Config_->EnableCompression) {
+ openMode = OpenAlways|RdWr|CloseOnExec;
+ } else {
+ openMode = OpenAlways|ForAppend|WrOnly|Seq|CloseOnExec;
+ }
+
+ // Generate filename.
+ FileName_ = Config_->FileName;
+ if (Config_->UseTimestampSuffix) {
+ FileName_ += "." + LastRotationTimestamp_.ToStringLocalUpToSeconds();
+ }
+
+ File_.reset(new TFile(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)",
+ 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)",
+ FileName_);
+ } catch (...) {
+ YT_ABORT();
+ }
+ }
+
+ void Rotate()
+ {
+ try {
+ auto fileNames = ListFiles();
+ auto count = GetFileCountToKeep(fileNames);
+ for (int index = count; index < ssize(fileNames); ++index) {
+ auto filePath = NFS::CombinePaths(DirectoryName_, fileNames[index]);
+ YT_LOG_DEBUG("Remove log segement (FilePath: %v)", filePath);
+ NFS::Remove(filePath);
+ }
+ fileNames.resize(count);
+
+ RenameFiles(fileNames);
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Failed to rotate log files");
+ } catch (...) {
+ YT_ABORT();
+ }
+ }
+
+ std::vector<TString> ListFiles() const
+ {
+ auto files = NFS::EnumerateFiles(DirectoryName_);
+ std::erase_if(files, [&] (const TString& s) {
+ return !s.StartsWith(FileNamePrefix_);
+ });
+ if (Config_->UseTimestampSuffix) {
+ // Rotated files are suffixed with the date, decreasing with the age of file.
+ std::sort(files.begin(), files.end(), std::greater<TString>());
+ } else {
+ // Rotated files are suffixed with the number, increasing with the age of file.
+ std::sort(files.begin(), files.end());
+ }
+ return files;
+ }
+
+ int GetFileCountToKeep(const std::vector<TString>& fileNames) const
+ {
+ const auto& rotationPolicy = Config_->RotationPolicy;
+ int filesToKeep = 0;
+ i64 totalSize = 0;
+ for (const auto& fileName : fileNames) {
+ auto fileSize = NFS::GetPathStatistics(NFS::CombinePaths(DirectoryName_, fileName)).Size;
+ if (totalSize + fileSize > rotationPolicy->MaxTotalSizeToKeep ||
+ filesToKeep + 1 > rotationPolicy->MaxSegmentCountToKeep)
+ {
+ return filesToKeep;
+ }
+ ++filesToKeep;
+ totalSize += fileSize;
+ }
+ return fileNames.size();
+ }
+
+ void RenameFiles(const std::vector<TString>& fileNames)
+ {
+ if (Config_->UseTimestampSuffix) {
+ return;
+ }
+
+ int width = ToString(ssize(fileNames)).length();
+ TString formatString = "%v.%0" + ToString(width) + "d";
+ for (int index = ssize(fileNames); index > 0; --index) {
+ auto newFileName = Format(formatString, FileNamePrefix_, index);
+ auto oldPath = NFS::CombinePaths(DirectoryName_, fileNames[index - 1]);
+ auto newPath = NFS::CombinePaths(DirectoryName_, newFileName);
+ YT_LOG_DEBUG("Rename log segement (OldFilePath: %v, NewFilePath: %v)", oldPath, newPath);
+ NFS::Rename(oldPath, newPath);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+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..a8bf825af5
--- /dev/null
+++ b/yt/yt/core/logging/formatter.cpp
@@ -0,0 +1,218 @@
+#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
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPlainTextLogFormatter::TPlainTextLogFormatter(
+ bool enableSystemMessages,
+ bool enableSourceLocation)
+ : EnableSystemMessages_(enableSystemMessages && Logger)
+ , EventFormatter_(enableSourceLocation)
+{ }
+
+i64 TPlainTextLogFormatter::WriteFormatted(IOutputStream* outputStream, const TLogEvent& event)
+{
+ if (!outputStream) {
+ return 0;
+ }
+
+ Buffer_.Reset();
+
+ EventFormatter_.Format(&Buffer_, event);
+
+ outputStream->Write(Buffer_.GetData(), Buffer_.GetBytesWritten());
+
+ return Buffer_.GetBytesWritten();
+}
+
+void TPlainTextLogFormatter::WriteLogReopenSeparator(IOutputStream* outputStream)
+{
+ *outputStream << Endl;
+}
+
+void TPlainTextLogFormatter::WriteLogStartEvent(IOutputStream* outputStream)
+{
+ if (EnableSystemMessages_) {
+ WriteFormatted(outputStream, GetStartLogEvent());
+ }
+}
+
+void TPlainTextLogFormatter::WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy)
+{
+ if (EnableSystemMessages_) {
+ WriteFormatted(outputStream, GetSkippedLogEvent(count, skippedBy));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStructuredLogFormatter::TStructuredLogFormatter(
+ ELogFormat format,
+ THashMap<TString, NYTree::INodePtr> commonFields,
+ bool enableSystemMessages,
+ NJson::TJsonFormatConfigPtr jsonFormat)
+ : Format_(format)
+ , 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)
+{
+ 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*/)
+{ }
+
+void TStructuredLogFormatter::WriteLogStartEvent(IOutputStream* outputStream)
+{
+ if (EnableSystemMessages_) {
+ WriteFormatted(outputStream, GetStartLogStructuredEvent());
+ }
+}
+
+void TStructuredLogFormatter::WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy)
+{
+ 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..1be9994aa6
--- /dev/null
+++ b/yt/yt/core/logging/formatter.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include "config.h"
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <library/cpp/yt/logging/plain_text_formatter/formatter.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ILogFormatter
+{
+ virtual ~ILogFormatter() = default;
+
+ virtual i64 WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) = 0;
+ virtual void WriteLogReopenSeparator(IOutputStream* outputStream) = 0;
+ virtual void WriteLogStartEvent(IOutputStream* outputStream) = 0;
+ virtual void WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPlainTextLogFormatter
+ : public ILogFormatter
+{
+public:
+ explicit TPlainTextLogFormatter(
+ bool enableControlMessages = true,
+ bool enableSourceLocation = false);
+
+ i64 WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) override;
+ void WriteLogReopenSeparator(IOutputStream* outputStream) override;
+ void WriteLogStartEvent(IOutputStream* outputStream) override;
+ void WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) override;
+
+private:
+ const bool EnableSystemMessages_;
+
+ TRawFormatter<MessageBufferSize> Buffer_;
+ TPlainTextEventFormatter EventFormatter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+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) override;
+ void WriteLogReopenSeparator(IOutputStream* outputStream) override;
+ void WriteLogStartEvent(IOutputStream* outputStream) override;
+ void WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) override;
+
+private:
+ const ELogFormat Format_;
+ const THashMap<TString, NYTree::INodePtr> CommonFields_;
+ const bool EnableSystemMessages_;
+ const NJson::TJsonFormatConfigPtr JsonFormat_;
+
+ TCachingDateFormatter CachingDateFormatter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..976fe387bf
--- /dev/null
+++ b/yt/yt/core/logging/log_manager.cpp
@@ -0,0 +1,1603 @@
+#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))
+ , FileRotationExecutor_(New<TPeriodicExecutor>(
+ EventQueue_,
+ BIND(&TImpl::RotateFiles, 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_;
+
+ TEnqueuedAction CurrentAction_;
+
+ TClosure BeginExecute() override
+ {
+ VERIFY_THREAD_AFFINITY(Owner_->LoggingThread);
+
+ return BeginExecuteImpl(Owner_->EventQueue_->BeginExecute(&CurrentAction_), &CurrentAction_);
+ }
+
+ void EndExecute() override
+ {
+ VERIFY_THREAD_AFFINITY(Owner_->LoggingThread);
+
+ Owner_->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();
+ FileRotationExecutor_->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);
+ FileRotationExecutor_->SetPeriod(Config_->RotationCheckPeriod);
+
+ 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 RotateFiles()
+ {
+ for (const auto& [name, writer] : NameToWriter_) {
+ if (auto fileWriter = DynamicPointerCast<IFileLogWriter>(writer)) {
+ fileWriter->MaybeRotate();
+ }
+ }
+ }
+
+ 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);
+
+ // 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 TPeriodicExecutorPtr FileRotationExecutor_;
+
+ 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..50d7e0f56b
--- /dev/null
+++ b/yt/yt/core/logging/log_writer.h
@@ -0,0 +1,37 @@
+#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;
+ virtual void MaybeRotate() = 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/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..18f56f547f
--- /dev/null
+++ b/yt/yt/core/logging/public.h
@@ -0,0 +1,45 @@
+#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(TRotationPolicyConfig)
+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..bfc60b2d94
--- /dev/null
+++ b/yt/yt/core/logging/unittests/logging_ut.cpp
@@ -0,0 +1,1322 @@
+#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/fs.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 TLoggingTestBase
+{
+protected:
+ const int DateLength = ToString("2014-04-24 23:41:09,804000").length();
+
+ 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);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLoggingTest
+ : public ::testing::Test
+ , public TLoggingTestBase
+ , public ILogWriterHost
+{
+protected:
+ IInvokerPtr GetCompressionInvoker() override
+ {
+ return GetCurrentInvoker();
+ }
+
+ 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 TBuiltinRotationTest
+ : public ::testing::TestWithParam<bool>
+ , public TLoggingTestBase
+{
+protected:
+ std::vector<TString> ListLogFiles(const TString& fileNamePrefix, bool reverse = false)
+ {
+ auto files = NFS::EnumerateFiles("./");
+ std::erase_if(files, [&] (const TString& fileName) {
+ return !fileName.StartsWith(fileNamePrefix);
+ });
+ if (reverse) {
+ std::sort(files.begin(), files.end(), std::greater<TString>());
+ } else {
+ std::sort(files.begin(), files.end());
+ }
+ return files;
+ }
+
+};
+
+TEST_P(TBuiltinRotationTest, All)
+{
+ bool useTimestampSuffix = GetParam();
+ auto logFileNamePrefix = GenerateLogFileName();
+
+ // To ensure that renumeration of rotated files works.
+ const int RotationDepth = useTimestampSuffix ? 2 : 12;
+
+ Configure(Format(R"({
+ rotation_check_period = 1000;
+ flush_period = 100;
+ rules = [
+ {
+ "min_level" = "info";
+ "writers" = [ "info" ];
+ };
+ ];
+ "writers" = {
+ "info" = {
+ "file_name" = "%v";
+ "use_timestamp_suffix" = %v;
+ "type" = "file";
+ "rotation_policy" = {
+ "max_segment_count_to_keep" = %v;
+ "max_segment_size" = 10;
+ };
+ };
+ };
+ })", logFileNamePrefix, ConvertToYsonString(useTimestampSuffix).AsStringBuf(), RotationDepth));
+
+ std::vector<TString> messages;
+ for (int index = 0; index < RotationDepth + 3; ++index) {
+ auto message = Format("Message%v", index);
+ messages.push_back(message);
+
+ // Wait until the message hits the file.
+ WaitForPredicate([&] {
+ YT_LOG_INFO(message);
+ auto files = ListLogFiles(logFileNamePrefix, useTimestampSuffix);
+ if (files.empty()) {
+ return false;
+ }
+ return CheckPlainTextLogFileContains(files[0], message);
+ });
+
+ // Wait until the file is rotated.
+ WaitForPredicate([&] {
+ auto files = ListLogFiles(logFileNamePrefix, useTimestampSuffix);
+ EXPECT_FALSE(files.empty());
+ auto lines = ReadPlainTextEvents(files[0]);
+ return lines.empty();
+ });
+ }
+
+ auto files = ListLogFiles(logFileNamePrefix, useTimestampSuffix);
+ EXPECT_EQ(RotationDepth + 1, ssize(files));
+ // Current file is empty, previous file must contain the last logged message.
+ for (int index = 1; index < ssize(files); ++index) {
+ EXPECT_TRUE(CheckPlainTextLogFileContains(files[index], messages[messages.size() - index]));
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(ValueParametrized, TBuiltinRotationTest,
+::testing::Values(
+ true,
+ false));
+
+////////////////////////////////////////////////////////////////////////////////
+
+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/CMakeLists.txt b/yt/yt/core/misc/CMakeLists.txt
new file mode 100644
index 0000000000..3bbc8bf365
--- /dev/null
+++ b/yt/yt/core/misc/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(isa_crc64)
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..81b615158a
--- /dev/null
+++ b/yt/yt/core/misc/collection_helpers-inl.h
@@ -0,0 +1,354 @@
+#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>
+TKeySet DropMissingKeys(TMap&& map, const TKeySet& set)
+{
+ TKeySet dropped;
+ for (auto it = map.begin(); it != map.end(); ) {
+ if (!set.contains(it->first)) {
+ dropped.insert(it->first);
+ map.erase(it++);
+ } else {
+ ++it;
+ }
+ }
+ return dropped;
+}
+
+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..01e82a31b0
--- /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>
+TKeySet 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/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..7178d24803
--- /dev/null
+++ b/yt/yt/core/misc/error_code.cpp
@@ -0,0 +1,164 @@
+#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;
+ }
+ // TODO(yuryalekseev): Deal with duplicate SslError in NRpc and NBus.
+ if (code == 119) {
+ 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/CMakeLists.darwin-x86_64.txt b/yt/yt/core/misc/isa_crc64/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..2d7f56d1d4
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,30 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(isa-l_crc_yt_patch)
+target_compile_options(isa-l_crc_yt_patch PRIVATE
+ -mpclmul
+)
+target_link_libraries(isa-l_crc_yt_patch PUBLIC
+ contrib-libs-cxxsupp
+)
+target_sources(isa-l_crc_yt_patch PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/checksum.cpp
+)
+target_yasm_source(isa-l_crc_yt_patch
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/crc64_yt_norm_by8.asm
+ -I
+ ${CMAKE_BINARY_DIR}
+ -I
+ ${CMAKE_SOURCE_DIR}
+ -I
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/include
+)
diff --git a/yt/yt/core/misc/isa_crc64/CMakeLists.linux-aarch64.txt b/yt/yt/core/misc/isa_crc64/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..8b887f5027
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(isa-l_crc_yt_patch)
+target_link_libraries(isa-l_crc_yt_patch PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+)
+target_sources(isa-l_crc_yt_patch PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/checksum.cpp
+)
diff --git a/yt/yt/core/misc/isa_crc64/CMakeLists.linux-x86_64.txt b/yt/yt/core/misc/isa_crc64/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..cc4e93a333
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,31 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(isa-l_crc_yt_patch)
+target_compile_options(isa-l_crc_yt_patch PRIVATE
+ -mpclmul
+)
+target_link_libraries(isa-l_crc_yt_patch PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+)
+target_sources(isa-l_crc_yt_patch PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/checksum.cpp
+)
+target_yasm_source(isa-l_crc_yt_patch
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/crc64_yt_norm_by8.asm
+ -I
+ ${CMAKE_BINARY_DIR}
+ -I
+ ${CMAKE_SOURCE_DIR}
+ -I
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/include
+)
diff --git a/yt/yt/core/misc/isa_crc64/CMakeLists.txt b/yt/yt/core/misc/isa_crc64/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/core/misc/isa_crc64/CMakeLists.windows-x86_64.txt b/yt/yt/core/misc/isa_crc64/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..2d7f56d1d4
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,30 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(isa-l_crc_yt_patch)
+target_compile_options(isa-l_crc_yt_patch PRIVATE
+ -mpclmul
+)
+target_link_libraries(isa-l_crc_yt_patch PUBLIC
+ contrib-libs-cxxsupp
+)
+target_sources(isa-l_crc_yt_patch PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/checksum.cpp
+)
+target_yasm_source(isa-l_crc_yt_patch
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/crc64_yt_norm_by8.asm
+ -I
+ ${CMAKE_BINARY_DIR}
+ -I
+ ${CMAKE_SOURCE_DIR}
+ -I
+ ${CMAKE_SOURCE_DIR}/yt/yt/core/misc/isa_crc64/include
+)
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/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/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..de6cb2cd2a
--- /dev/null
+++ b/yt/yt/core/misc/unittests/hazard_ptr_ut.cpp
@@ -0,0 +1,390 @@
+#include <yt/yt/core/test_framework/framework.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 <yt/yt/core/concurrency/delayed_executor.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_;
+ int AllocatedCount_ = 0;
+ int 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_;
+};
+
+class THazardPtrTest
+ : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ {
+ // Ensure that delete list is empty.
+ ReclaimHazardPointers();
+ }
+};
+
+TEST_F(THazardPtrTest, RefCountedPtrBehavior)
+{
+ 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_F(THazardPtrTest, DelayedDeallocation)
+{
+ 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_F(THazardPtrTest, DelayedDeallocationWithMultipleHPs)
+{
+ 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_F(THazardPtrTest, CombinedLogic)
+{
+ 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());
+ }
+}
+
+TEST_W(THazardPtrTest, ThreadMaintenance)
+{
+ TStringStream output;
+ TTestAllocator allocator(&output);
+
+ {
+ auto ptr = New<TSampleObject>(&allocator, &output);
+ ptr->DoSomething();
+ }
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ NConcurrency::TDelayedExecutor::WaitForDuration(TDuration::Seconds(3));
+
+ 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_F(THazardPtrTest, DelayedDeallocationPolymorphic)
+{
+ 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_F(THazardPtrTest, SupportFork)
+{
+ 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..0de99de730
--- /dev/null
+++ b/yt/yt/core/misc/unittests/ya.make
@@ -0,0 +1,103 @@
+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
+ 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
+ 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..e80a14acb8
--- /dev/null
+++ b/yt/yt/core/net/address.cpp
@@ -0,0 +1,1270 @@
+#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));
+ 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..0771c9807f
--- /dev/null
+++ b/yt/yt/core/net/config.cpp
@@ -0,0 +1,53 @@
+#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("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..8424b1e928
--- /dev/null
+++ b/yt/yt/core/net/config.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt/core/misc/cache_config.h>
+
+#include <yt/yt/core/dns/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 NDns::TAresDnsResolverConfig
+{
+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;
+ //! 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..3b9b6b0615
--- /dev/null
+++ b/yt/yt/core/net/listener.cpp
@@ -0,0 +1,239 @@
+#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..273efe80a8
--- /dev/null
+++ b/yt/yt/core/profiling/tscp-inl.h
@@ -0,0 +1,62 @@
+#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_
+
+#include <util/system/cpu_id.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#if defined(__x86_64__)
+
+const bool SupportsRdtscp = NX86::HaveRDTSCP();
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+inline TTscp TTscp::Get()
+{
+#if defined(__x86_64__)
+ ui64 t;
+ ui64 c;
+ if (SupportsRdtscp) {
+ ui64 rax, rcx, rdx;
+ asm volatile ( "rdtscp\n" : "=a" (rax), "=c" (rcx), "=d" (rdx) : : );
+ t = (rdx << 32) + rax;
+ c = rcx;
+ } else {
+ ui64 rax, rdx;
+ asm volatile ( "rdtsc\n" : "=a" (rax), "=d" (rdx) : : );
+ t = (rdx << 32) + rax;
+
+ // cpuId[1] >> 24 is an APIC id.
+ ui32 cpuId[4];
+ NX86::CpuId(1, 0, cpuId);
+ c = cpuId[1] >> 24;
+ }
+#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..5b44257f6a
--- /dev/null
+++ b/yt/yt/core/rpc/client.h
@@ -0,0 +1,511 @@
+#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_GENERIC(method, request, response, ...) \
+ using TRsp##method = ::NYT::NRpc::TTypedClientResponse<response>; \
+ using TReq##method = ::NYT::NRpc::TTypedClientRequest<request, 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)
+
+#define DEFINE_RPC_PROXY_METHOD(ns, method, ...) \
+ DEFINE_RPC_PROXY_METHOD_GENERIC(method, ns::TReq##method, ns::TRsp##method, __VA_ARGS__)
+
+////////////////////////////////////////////////////////////////////////////////
+
+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..6b117e69e7
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/channel.cpp
@@ -0,0 +1,734 @@
+#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();
+ }
+
+ void RecordAnnotation(y_absl::string_view /*annotation*/) override
+ { }
+
+private:
+ class TAttemptTracer
+ : public CallAttemptTracer
+ {
+ public:
+ TAttemptTracer()
+ : Error_(y_absl::OkStatus())
+ { }
+
+ ~TAttemptTracer() override
+ { }
+
+ 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);
+ }
+
+ void RecordEnd(const gpr_timespec& /*latency*/) override
+ { }
+
+ void RecordAnnotation(y_absl::string_view /*annotation*/) override
+ { }
+
+ TError GetError()
+ {
+ auto error = Error_.get();
+ intptr_t statusCode;
+ if (!grpc_error_get_int(error, grpc_core::StatusIntProperty::kRpcStatus, &statusCode)) {
+ statusCode = GRPC_STATUS_UNKNOWN;
+ }
+ TString statusDetail;
+ if (!grpc_error_get_str(error, grpc_core::StatusStrProperty::kDescription, &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 (credentialsExt.has_service_ticket()) {
+ InitialMetadataBuilder_.Add(AuthServiceTicketMetadataKey, credentialsExt.service_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.
+ TGuardedGrpcCompletionQueue* 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..25ae7734c9
--- /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;
+ }
+
+ TGuardedGrpcCompletionQueue* 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();
+ }
+
+ TGuardedGrpcCompletionQueue* GetGuardedCompletionQueue()
+ {
+ return &GuardedCompletionQueue_;
+ }
+
+ private:
+ TGrpcLibraryLockPtr LibraryLock_;
+ TGuardedGrpcCompletionQueue 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();
+}
+
+TGuardedGrpcCompletionQueue* 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..a184074d3a
--- /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();
+ TGuardedGrpcCompletionQueue* 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..2548f40e4f
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/helpers.cpp
@@ -0,0 +1,583 @@
+#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);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGuardedGrpcCompletionQueue::TGuardedGrpcCompletionQueue(TGrpcCompletionQueuePtr completionQueue)
+ : CompletionQueue_(std::move(completionQueue))
+{ }
+
+std::optional<TGuardedGrpcCompletionQueue::TLockGuard> TGuardedGrpcCompletionQueue::TryLock()
+{
+ auto guard = ReaderGuard(SpinLock_);
+ if (State_ != EState::Opened) {
+ return std::nullopt;
+ }
+ LocksCount_.fetch_add(1, std::memory_order::acquire);
+ return std::optional<TGuardedGrpcCompletionQueue::TLockGuard>(this);
+}
+
+void TGuardedGrpcCompletionQueue::Shutdown()
+{
+ {
+ auto guard = WriterGuard(SpinLock_);
+ if (State_ == EState::Shutdown) {
+ return;
+ }
+ State_ = EState::Shutdown;
+ if (LocksCount_ != 0) {
+ guard.Release();
+ ReleaseDone_.Wait();
+ }
+ }
+ grpc_completion_queue_shutdown(CompletionQueue_.Unwrap());
+}
+
+void TGuardedGrpcCompletionQueue::Reset()
+{
+ auto guard = WriterGuard(SpinLock_);
+ YT_VERIFY(State_ == EState::Shutdown);
+ CompletionQueue_.Reset();
+}
+
+grpc_completion_queue* TGuardedGrpcCompletionQueue::UnwrapUnsafe()
+{
+ return CompletionQueue_.Unwrap();
+}
+
+void TGuardedGrpcCompletionQueue::Release()
+{
+ auto guard = ReaderGuard(SpinLock_);
+ if (LocksCount_.fetch_sub(1, std::memory_order::release) == 0 && State_ == EState::Shutdown) {
+ guard.Release();
+ ReleaseDone_.NotifyOne();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..3c28b54277
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/helpers.h
@@ -0,0 +1,299 @@
+#pragma once
+
+#include "private.h"
+
+#include <yt/yt/core/crypto/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+#include <library/cpp/yt/threading/rw_spin_lock.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 TGuardedGrpcCompletionQueue
+{
+public:
+ explicit TGuardedGrpcCompletionQueue(TGrpcCompletionQueuePtr completionQueuePtr);
+
+ class TLockGuard
+ {
+ public:
+ explicit TLockGuard(TGuardedGrpcCompletionQueue* guardedCompletionQueue)
+ : GuardedCompletionQueue_(guardedCompletionQueue)
+ { }
+
+ TLockGuard(TLockGuard&& guard) = default;
+ TLockGuard& operator=(TLockGuard&& guard) = default;
+
+ ~TLockGuard()
+ {
+ Unlock();
+ }
+
+ grpc_completion_queue* Unwrap()
+ {
+ return GuardedCompletionQueue_->UnwrapUnsafe();
+ }
+
+ void Unlock()
+ {
+ if (GuardedCompletionQueue_) {
+ GuardedCompletionQueue_->Release();
+ }
+ }
+
+ private:
+ TGuardedGrpcCompletionQueue* GuardedCompletionQueue_;
+ };
+
+ std::optional<TLockGuard> 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 CompletionQueue_;
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ EState State_ = EState::Opened;
+ std::atomic<int> LocksCount_ = 0;
+ NThreading::TEvent ReleaseDone_;
+
+ void Release();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+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..5b94a713cf
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/public.cpp
@@ -0,0 +1,28 @@
+#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 AuthServiceTicketMetadataKey = "yt-auth-service-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..4eb8a13014
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/public.h
@@ -0,0 +1,41 @@
+#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 AuthServiceTicketMetadataKey;
+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..5df09d986a
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/server.cpp
@@ -0,0 +1,1105 @@
+#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;
+ }
+
+ bool IsEncrypted() 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);
+ auto serviceTicketString = CallMetadata_.Find(AuthServiceTicketMetadataKey);
+
+ if (!tokenString &&
+ !legacyTokenString &&
+ !sessionIdString &&
+ !sslSessionIdString &&
+ !userTicketString &&
+ !serviceTicketString)
+ {
+ 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));
+ }
+ if (serviceTicketString) {
+ RpcCredentialsExt_->set_service_ticket(TString(serviceTicketString));
+ }
+ }
+
+ 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..27a41b8976
--- /dev/null
+++ b/yt/yt/core/rpc/local_channel.cpp
@@ -0,0 +1,373 @@
+#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;
+ }
+
+ bool IsEncrypted() const override
+ {
+ return false;
+ }
+
+ 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..616bfce116
--- /dev/null
+++ b/yt/yt/core/rpc/public.h
@@ -0,0 +1,192 @@
+#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))
+ ((Overloaded) (118)) // The server is currently overloaded and unable to handle additional requests.
+ // The client should try to reduce their request rate until the server has had a chance to recover.
+ ((SslError) (static_cast<int>(NBus::EErrorCode::SslError)))
+);
+
+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..3f862ca176
--- /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.TimeHistogram("/request_time_histogram/execution", customBounds);
+ RemoteWaitTimeCounter = profiler.TimeHistogram("/request_time_histogram/remote_wait", customBounds);
+ LocalWaitTimeCounter = profiler.TimeHistogram("/request_time_histogram/local_wait", customBounds);
+ TotalTimeCounter = profiler.TimeHistogram("/request_time_histogram/total", customBounds);
+ } else if (histogramConfig && histogramConfig->ExponentialBounds) {
+ const auto &exponentialBounds = *histogramConfig->ExponentialBounds;
+ ExecutionTimeCounter = profiler.TimeHistogram("/request_time_histogram/execution", exponentialBounds->Min, exponentialBounds->Max);
+ RemoteWaitTimeCounter = profiler.TimeHistogram("/request_time_histogram/remote_wait", exponentialBounds->Min, exponentialBounds->Max);
+ LocalWaitTimeCounter = profiler.TimeHistogram("/request_time_histogram/local_wait", exponentialBounds->Min, exponentialBounds->Max);
+ TotalTimeCounter = profiler.TimeHistogram("/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..6dd81db2be
--- /dev/null
+++ b/yt/yt/core/rpc/service_detail.h
@@ -0,0 +1,1097 @@
+#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);
+
+protected:
+ void ReplyError(
+ TError error,
+ const NProto::TRequestHeader& header,
+ const NYT::NBus::IBusPtr& replyBus);
+
+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 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..884956c830
--- /dev/null
+++ b/yt/yt/core/ya.make
@@ -0,0 +1,392 @@
+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
+ bus/tcp/ssl_context.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/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/config.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
+ contrib/libs/openssl
+
+ library/cpp/openssl/init
+ library/cpp/threading/thread_local
+ library/cpp/streams/brotli
+ library/cpp/yt/assert
+ library/cpp/yt/containers
+ library/cpp/yt/logging
+ library/cpp/yt/logging/plain_text_formatter
+ 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
+ test_framework
+)
+
+IF (NOT OPENSOURCE)
+ RECURSE(
+ benchmarks
+ bus/benchmarks
+ yson/benchmark
+ )
+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..4d77ff85f7
--- /dev/null
+++ b/yt/yt/core/yson/detail.h
@@ -0,0 +1,1009 @@
+#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;
+
+ const i64 MemoryLimit_;
+
+ std::vector<char> Buffer_;
+
+ 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)
+ {
+ 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 (minReserveSize > static_cast<size_t>(MemoryLimit_)) {
+ THROW_ERROR_EXCEPTION(
+ "Memory limit exceeded while parsing YSON stream: allocated %v, limit %v",
+ minReserveSize,
+ MemoryLimit_);
+ }
+
+ // MemoryLimit_ >= minReserveSize ==> reserveSize >= minReserveSize
+ reserveSize = Min(reserveSize, static_cast<size_t>(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..e23e0bd5c1
--- /dev/null
+++ b/yt/yt/core/yson/lexer.cpp
@@ -0,0 +1,25 @@
+#include "lexer.h"
+#include "lexer_detail.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TStatelessLexer::ParseToken(TStringBuf data, TToken* token)
+{
+ Lexer_.SetBuffer(data.begin(), data.end());
+ Lexer_.ParseToken(token);
+ return Lexer_.Current() - data.begin();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t ParseToken(TStringBuf data, TToken* token)
+{
+ TStatelessLexer lexer;
+ return lexer.ParseToken(data, token);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..1d38ca1327
--- /dev/null
+++ b/yt/yt/core/yson/lexer.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "token.h"
+#include "lexer_detail.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatelessLexer
+{
+public:
+ size_t ParseToken(TStringBuf data, TToken* token);
+
+private:
+ NDetail::TLexer<TStringReader, false> Lexer_{TStringReader()};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t ParseToken(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..96c2967a89
--- /dev/null
+++ b/yt/yt/core/yson/lexer_detail.h
@@ -0,0 +1,274 @@
+#pragma once
+
+#include "detail.h"
+#include "token.h"
+
+namespace NYT::NYson::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 BF
+#undef BT
+#undef BU
+#undef SP
+#undef DM
+#undef ST
+#undef PL
+#undef QU
+#undef PC
+#undef TT
+ return lookupTable[static_cast<ui8>(ch)];
+ }
+
+public:
+ TLexer(
+ const TBlockStream& blockStream,
+ i64 memoryLimit = std::numeric_limits<i64>::max())
+ : TBase(blockStream, memoryLimit)
+ { }
+
+ void ParseToken(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);
+ auto value = TBase::ReadQuotedString();
+ *token = TToken(value, /*binaryString*/ false);
+ } 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) {
+ auto value = TBase::template ReadUnquotedString<true>();
+ *token = TToken(value, /*binaryString*/ false);
+ } 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));
+ auto value = TBase::ReadBinaryString();
+ *token = TToken(value, /*binaryString*/ true);
+ }
+ }
+ }
+
+ template <bool AllowFinish>
+ void ReadNumeric(TToken* token)
+ {
+ TStringBuf valueBuffer;
+ auto 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 NYT::NYson::NDetail
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..6380193283
--- /dev/null
+++ b/yt/yt/core/yson/parser_detail.h
@@ -0,0 +1,529 @@
+#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* const Consumer_;
+ const int NestingLevelLimit_;
+
+ int NestingLevel_ = 0;
+ 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..56d496fbee
--- /dev/null
+++ b/yt/yt/core/yson/public.h
@@ -0,0 +1,70 @@
+#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 TStatelessLexer;
+
+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..9c32a97078
--- /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)
+{ }
+
+size_t 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)
+{ }
+
+size_t 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..ed704ac1b0
--- /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;
+ size_t 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;
+
+ size_t 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..02abc1e52a
--- /dev/null
+++ b/yt/yt/core/yson/token.cpp
@@ -0,0 +1,239 @@
+#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)
+{ }
+
+TToken::TToken(ETokenType type)
+ : Type_(type)
+{
+ 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, bool binaryString)
+ : Type_(ETokenType::String)
+ , StringValue_(stringValue)
+ , BinaryString_(binaryString)
+{ }
+
+TToken::TToken(i64 int64Value)
+ : Type_(ETokenType::Int64)
+ , Int64Value_(int64Value)
+{ }
+
+TToken::TToken(ui64 uint64Value)
+ : Type_(ETokenType::Uint64)
+ , Uint64Value_(uint64Value)
+{ }
+
+TToken::TToken(double doubleValue)
+ : Type_(ETokenType::Double)
+ , DoubleValue_(doubleValue)
+{ }
+
+TToken::TToken(bool booleanValue)
+ : Type_(ETokenType::Boolean)
+ , BooleanValue_(booleanValue)
+{ }
+
+bool TToken::IsEmpty() const
+{
+ return Type_ == ETokenType::EndOfStream;
+}
+
+TStringBuf TToken::GetStringValue() const
+{
+ ExpectType(ETokenType::String);
+ return StringValue_;
+}
+
+bool TToken::IsBinaryString() const
+{
+ ExpectType(ETokenType::String);
+ return BinaryString_;
+}
+
+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..c9da4c9272
--- /dev/null
+++ b/yt/yt/core/yson/token.h
@@ -0,0 +1,91 @@
+#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
+ TToken(TStringBuf stringValue, bool binaryString); // 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;
+ bool IsBinaryString() 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_;
+ bool BinaryString_ = false;
+ i64 Int64Value_ = 0;
+ ui64 Uint64Value_ = 0;
+ double DoubleValue_ = 0;
+ bool BooleanValue_ = false;
+};
+
+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..e8de924eba
--- /dev/null
+++ b/yt/yt/core/yson/tokenizer.cpp
@@ -0,0 +1,47 @@
+#include "tokenizer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTokenizer::TTokenizer(TStringBuf input)
+ : Input_(input)
+{ }
+
+bool TTokenizer::ParseNext()
+{
+ Input_ = Input_.Tail(Parsed_);
+ Token_.Reset();
+ Parsed_ = Lexer_.ParseToken(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..962eecfa83
--- /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_ = 0;
+ size_t Position_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // 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..0709b65bd5
--- /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->ParseToken(input, &token);
+ }
+
+ TToken GetToken(TStringBuf input)
+ {
+ TToken token;
+ Lexer->ParseToken(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..c2a291356f
--- /dev/null
+++ b/yt/yt/core/yson/unittests/ypath_designated_yson_consumer_ut.cpp
@@ -0,0 +1,243 @@
+#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);
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // 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..7cb13fab1f
--- /dev/null
+++ b/yt/yt/core/yson/ypath_designated_consumer.cpp
@@ -0,0 +1,232 @@
+#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) {
+ Tokenizer_.Skip(NYPath::ETokenType::Slash);
+ CurrentListItemIndex_ = -1;
+ }
+ }
+
+ void OnMyListItem() override
+ {
+ if (State_ == EConsumerState::ConsumptionCompleted) {
+ return;
+ }
+
+ ++CurrentListItemIndex_;
+ switch (Tokenizer_.GetType()) {
+ case NYPath::ETokenType::Literal:
+ int indexInPath;
+ if (!TryFromString(Tokenizer_.GetToken(), indexInPath)) {
+ CheckAndThrowResolveError();
+ }
+ if (indexInPath == CurrentListItemIndex_) {
+ Tokenizer_.Advance();
+ ForwardIfPathIsExhausted();
+ } else {
+ SkipSubTree();
+ }
+ break;
+ default:
+ Tokenizer_.ThrowUnexpected();
+ }
+ }
+
+ void OnMyEndList() override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ 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 {
+ SkipSubTree();
+ }
+ 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 {
+ SkipSubTree(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;
+ int CurrentListItemIndex_ = -1;
+
+ 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; });
+ }
+ }
+
+ void SkipSubTree(EYsonType type = EYsonType::Node)
+ {
+ Forward(&NullConsumer_, /*onFinished*/ nullptr, type);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+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/lazy_ypath_service_ut.cpp b/yt/yt/core/ytree/unittests/lazy_ypath_service_ut.cpp
new file mode 100644
index 0000000000..c8b3d1a518
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/lazy_ypath_service_ut.cpp
@@ -0,0 +1,331 @@
+#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(TLazyYPathServiceTest, TestGetSelf)
+{
+ auto service = IYPathService::FromProducerLazy(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .EndMap();
+ }));
+
+ EXPECT_EQ(FluentString().BeginMap().Item("key1").Value(42).EndMap(), YPathGet(service, ""));
+}
+
+TEST(TLazyYPathServiceTest, SimpleTypes)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, QueryNestedKeySimple)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, QueryNestedComplex)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, GetList)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, NavigateThroughList)
+{
+ auto service = IYPathService::FromProducerLazy(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").BeginMap()
+ .Item("subkey1").Value("ab")
+ .EndMap()
+ .Item("key2").BeginList()
+ .Item().Value("abc")
+ .Item().Value(43)
+ .EndList()
+ .EndMap();
+ }));
+
+ EXPECT_EQ(FluentString().Value("abc"), YPathGet(service, "/key2/0"));
+ EXPECT_EQ(FluentString().Value(43), YPathGet(service, "/key2/1"));
+}
+
+TEST(TLazyYPathServiceTest, NavigateThroughTwoLists)
+{
+ auto service = IYPathService::FromProducerLazy(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginList()
+ .Item().BeginList()
+ .Item().Value(43)
+ .Item().Value("abc")
+ .EndList()
+ .Item().BeginList()
+ .Item().Value("def")
+ .EndList()
+ .EndList();
+ }));
+
+ EXPECT_EQ(FluentString().Value("abc"), YPathGet(service, "/0/1"));
+ EXPECT_EQ(FluentString().Value("def"), YPathGet(service, "/1/0"));
+}
+
+TEST(TLazyYPathServiceTest, GetAttributes)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, InexistentPaths)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, ExistsVerb)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, ListVerb)
+{
+ auto service = IYPathService::FromProducerLazy(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(TLazyYPathServiceTest, RootAttributes)
+{
+ auto service = IYPathService::FromProducerLazy(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/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..54684cbe9c
--- /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
+ lazy_ypath_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/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..142834c1d3
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_client.cpp
@@ -0,0 +1,932 @@
+#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();
+ }
+
+ factory->Commit();
+}
+
+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..6cc9caf9ff
--- /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::FromProducerLazy(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..d294da6d01
--- /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 lazy YPath service.
+ */
+ static IYPathServicePtr FromProducerLazy(
+ 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..2dfa8f5990
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct.cpp
@@ -0,0 +1,211 @@
+#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) 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();
+ SortBy(unrecognizedList, [] (const auto& item) { return item.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..7d336bd6c8
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct.h
@@ -0,0 +1,311 @@
+#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) 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_
diff --git a/yt/yt/library/CMakeLists.txt b/yt/yt/library/CMakeLists.txt
new file mode 100644
index 0000000000..20f7fe76fa
--- /dev/null
+++ b/yt/yt/library/CMakeLists.txt
@@ -0,0 +1,14 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(profiling)
+add_subdirectory(syncmap)
+add_subdirectory(tracing)
+add_subdirectory(tvm)
+add_subdirectory(undumpable)
+add_subdirectory(ytprof)
diff --git a/yt/yt/library/numeric/algorithm_helpers-inl.h b/yt/yt/library/numeric/algorithm_helpers-inl.h
new file mode 100644
index 0000000000..ea04df3a32
--- /dev/null
+++ b/yt/yt/library/numeric/algorithm_helpers-inl.h
@@ -0,0 +1,137 @@
+#ifndef ALGORITHM_HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include algorithm_helpers.h"
+// For the sake of sane code completion.
+#include "algorithm_helpers.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TIter, class TPredicate>
+Y_FORCE_INLINE TIter LinearSearch(TIter begin, TIter end, TPredicate pred)
+{
+ while (begin != end && pred(begin)) {
+ ++begin;
+ }
+
+ return begin;
+}
+
+template <class TIter, class TPredicate>
+TIter BinarySearch(TIter begin, TIter end, TPredicate pred)
+{
+ size_t count = end - begin;
+ while (count != 0) {
+ auto half = count / 2;
+ auto middle = begin + half;
+ if (pred(middle)) {
+ begin = ++middle;
+ count -= half + 1;
+ } else {
+ count = half;
+ }
+ }
+
+ return begin;
+}
+
+template <class TPredicate>
+size_t BinarySearch(size_t begin, size_t end, TPredicate pred)
+{
+ return NYT::BinarySearch<size_t, TPredicate>(begin, end, pred);
+}
+
+template <class TIter, class TPredicate>
+Y_FORCE_INLINE TIter ExponentialSearch(TIter begin, TIter end, TPredicate pred)
+{
+ size_t step = 1;
+ auto next = begin;
+
+ if (begin == end) {
+ return begin;
+ }
+
+ while (pred(next)) {
+ begin = ++next;
+
+ if (step < static_cast<size_t>(end - next)) {
+ next += step;
+ step *= 3;
+ } else {
+ next = end;
+ break;
+ }
+ }
+
+ return BinarySearch(begin, next, pred);
+}
+
+template <class TIter, class T>
+TIter LowerBound(TIter begin, TIter end, const T& value)
+{
+ return NYT::BinarySearch(begin, end, [&] (TIter it) {
+ return *it < value;
+ });
+}
+
+template <class TIter, class T>
+TIter UpperBound(TIter begin, TIter end, const T& value)
+{
+ return BinarySearch(begin, end, [&] (TIter it) {
+ return !(value < *it);
+ });
+}
+
+template <class TIter, class T>
+TIter ExpLowerBound(TIter begin, TIter end, const T& value)
+{
+ return ExponentialSearch(begin, end, [&] (TIter it) {
+ return *it < value;
+ });
+}
+
+template <class TIter, class T>
+TIter ExpUpperBound(TIter begin, TIter end, const T& value)
+{
+ return ExponentialSearch(begin, end, [&] (TIter it) {
+ return !(value < *it);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TInputIt1, class TInputIt2>
+bool Intersects(TInputIt1 first1, TInputIt1 last1, TInputIt2 first2, TInputIt2 last2)
+{
+ while (first1 != last1 && first2 != last2) {
+ if (*first1 < *first2) {
+ ++first1;
+ } else if (*first2 < *first1) {
+ ++first2;
+ } else {
+ return true;
+ }
+
+ }
+ return false;
+}
+
+template <class TIter>
+void PartialShuffle(TIter begin, TIter end, TIter last)
+{
+ while (begin != end) {
+ std::iter_swap(begin, begin + rand() % std::distance(begin, last));
+ ++begin;
+ }
+}
+
+template <class T, class TGetKey>
+std::pair<const T&, const T&> MinMaxBy(const T& first, const T& second, const TGetKey& getKey)
+{
+ return std::minmax(first, second, [&](auto&& left, auto&& right) { return getKey(left) < getKey(right); });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/library/numeric/algorithm_helpers.h b/yt/yt/library/numeric/algorithm_helpers.h
new file mode 100644
index 0000000000..d12165688e
--- /dev/null
+++ b/yt/yt/library/numeric/algorithm_helpers.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <algorithm>
+
+#include <util/system/compiler.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TIter, class TPredicate>
+TIter LinearSearch(TIter begin, TIter end, TPredicate pred);
+
+// Search returns iterator to the first element that is !pred(it).
+// Reverse(Search) returns iterator to the last element that pred(it).
+
+// With default predicate you can use shortcuts LowerBound and UpperBound.
+// If custom predicate is needed you should use Binary/ExponentialSearch because LowerBound and UpperBound is
+// determined by predicate (see implementation).
+
+// Properties of Binary/ExponentialSearch:
+// let Reverse = std::make_reverse_iterator
+// let Search = Binary/ExponentialSearch
+// Reverse(Search(begin, end, pred)) ~ Search(Reverse(end), Reverse(begin), [&] (auto it) { return !pred(it); })
+// Use first case due to reverse iterator contains extra fields (see LWG #2360).
+// Do not use it - 1 pattern (i.e. LowerBound(...) - 1). Convert iterator to reverse and compare with .rend()
+// instead. it - 1 causes STL debug assert violations when it is equal to .begin().
+
+template <class TIter, class TPredicate>
+TIter BinarySearch(TIter begin, TIter end, TPredicate pred);
+
+template <class TIter, class TPredicate>
+TIter ExponentialSearch(TIter begin, TIter end, TPredicate pred);
+
+// Properties based on property a <= b ~ !(b < a):
+// UpperBound(a < b) ~ LowerBound(a <= b)
+// UpperBound(a <= b) ~ LowerBound(a < b)
+
+template <class TIter, class T>
+TIter LowerBound(TIter begin, TIter end, const T& value);
+
+template <class TIter, class T>
+TIter UpperBound(TIter begin, TIter end, const T& value);
+
+template <class TIter, class T>
+TIter ExpLowerBound(TIter begin, TIter end, const T& value);
+
+template <class TIter, class T>
+TIter ExpUpperBound(TIter begin, TIter end, const T& value);
+
+//! Similar to std::set_intersection, but returns bool and doesn't output the
+//! actual intersection.
+//! Input ranges must be sorted.
+template <class TInputIt1, class TInputIt2>
+bool Intersects(TInputIt1 first1, TInputIt1 last1, TInputIt2 first2, TInputIt2 last2);
+
+//! Runs Fisher--Yates shuffle to pick a random K-subset [begin, end) from [begin,last) (for K = std::distance(begin, end)).
+template <class TIter>
+void PartialShuffle(TIter begin, TIter end, TIter last);
+
+template <class T, typename TGetKey>
+std::pair<const T&, const T&> MinMaxBy(const T& first, const T& second, const TGetKey& getKey);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define ALGORITHM_HELPERS_INL_H_
+#include "algorithm_helpers-inl.h"
+#undef ALGORITHM_HELPERS_INL_H_
diff --git a/yt/yt/library/profiling/CMakeLists.darwin-x86_64.txt b/yt/yt/library/profiling/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..bafd86d310
--- /dev/null
+++ b/yt/yt/library/profiling/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,31 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(resource_tracker)
+
+add_library(yt-library-profiling)
+target_compile_options(yt-library-profiling PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-profiling PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-small_containers
+ cpp-yt-string
+ cpp-yt-memory
+)
+target_sources(yt-library-profiling PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/sensor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/tag.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/testing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/histogram_snapshot.cpp
+)
diff --git a/yt/yt/library/profiling/CMakeLists.linux-aarch64.txt b/yt/yt/library/profiling/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..e4524762fc
--- /dev/null
+++ b/yt/yt/library/profiling/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(resource_tracker)
+
+add_library(yt-library-profiling)
+target_compile_options(yt-library-profiling PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-profiling PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-small_containers
+ cpp-yt-string
+ cpp-yt-memory
+)
+target_sources(yt-library-profiling PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/sensor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/tag.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/testing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/histogram_snapshot.cpp
+)
diff --git a/yt/yt/library/profiling/CMakeLists.linux-x86_64.txt b/yt/yt/library/profiling/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..e4524762fc
--- /dev/null
+++ b/yt/yt/library/profiling/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,32 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(resource_tracker)
+
+add_library(yt-library-profiling)
+target_compile_options(yt-library-profiling PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-profiling PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-small_containers
+ cpp-yt-string
+ cpp-yt-memory
+)
+target_sources(yt-library-profiling PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/sensor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/tag.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/testing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/histogram_snapshot.cpp
+)
diff --git a/yt/yt/library/profiling/CMakeLists.txt b/yt/yt/library/profiling/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/library/profiling/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/library/profiling/CMakeLists.windows-x86_64.txt b/yt/yt/library/profiling/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..6a6f1a6dde
--- /dev/null
+++ b/yt/yt/library/profiling/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,28 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(resource_tracker)
+
+add_library(yt-library-profiling)
+target_link_libraries(yt-library-profiling PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-cpu_clock
+ cpp-yt-small_containers
+ cpp-yt-string
+ cpp-yt-memory
+)
+target_sources(yt-library-profiling PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/sensor.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/producer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/impl.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/tag.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/testing.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/histogram_snapshot.cpp
+)
diff --git a/yt/yt/library/profiling/README.md b/yt/yt/library/profiling/README.md
new file mode 100644
index 0000000000..8fafcf23f9
--- /dev/null
+++ b/yt/yt/library/profiling/README.md
@@ -0,0 +1,144 @@
+# NYT::NProfiling
+
+yt/yt/library/profiling - это библиотека для мониторинга, которая используется
+в кодовой базе YT.
+
+Основные фичи:
+* Локальное вычисление агрегатов позволяет удобно работать с многомерными счётчиками.
+* Lockfree регистрация счётчиков позволяет создавать новые счётчики во время работы
+ процесса и не беспокоиться о лишних локах.
+* Внутренний буффер точек позволяет сохранять значения с высоким разрешением.
+ YT использует шаг сетки в 5c, при интервале сбора метрик 30с.
+* Sparse счётчики и де-регистрация старых счётчиков позволяет использовать
+ теги большой кардинальности, вроде `user` или `table_path`.
+* Опциональный глобальный registry позволяет добавлять счётчики в код,
+ без необходимости пробрасывать параметр через конструкторы.
+* Разделение на интерфейс и реализацию позволяет использовать интерфейс
+ в других core библиотеках, не создавая dependency hell.
+* Встроенная debug страница для отладки переполнения числа сенсоров
+ и мониторинга сбора метрик.
+* Умеет помечать сенсоры, для которых имеет смысл вычислять агрегат на стороне Соломона.
+ Эта опция спасает от безумных графиков "суммы максимумов по кластеру".
+
+## Несовместимости с yt/yt/core/profiling
+
+* Метрики, значением которых является интервал времени,
+ всегда экспортируются в секундах. `core/profiling` не имел такого понятия,
+ и в некоторых случаях требовал конвертации на стороне пользователя.
+ Дефолтная функция конвертации использовала микросекунды.
+
+## Сравнение от monlib
+
+| | YT | monlib |
+| ------------------------- | --- | ------ |
+| Spack | Yes | Yes |
+| Debug Page | Yes | Yes |
+| 5s Grid Step | Yes | No |
+| Local Projections | Yes | No |
+| Sparse Sensors | Yes | No |
+| Sensor Deregistration | Yes | No |
+
+## Разделение на интерфейс и реализацию
+
+`yt/yt/library/profiling` содержит минимальный интерфейс для добавления сенсоров в код.
+От этой библиотеки зависят другие `core` библиотеки.
+
+Реализация интерфейса находится в `yt/yt/library/profiling/solomon`. Интерфейс
+находит реализацию через `weak` символ `GetGlobalRegistry`. Если реализация не
+прилинкована, вся библиотека превращается в no-op.
+
+## Sparse сенсоры
+
+По умолчанию все сенсоры являются dense. Значение счётчика будет экспортироваться в Соломон,
+даже если этот сенсор не менялся или равен нулю. Это верно даже для сенсоров, которые
+создаются через `ISensorProducer`.
+
+В некоторых ситуациях такое поведение создаёт слишком большую нагрузку на Соломон. Например,
+среди сенсоров `request_count` пользователей мастера Хана, в каждый момент времени
+не равны нулю только несколько сотен из 10 тысяч. В таких случаях сенсор можно переключить
+в sparse режим.
+
+Sparse сенсоры не экспортируются, пока их значение равно нулю. После того, как значение сенсора
+становится не равно нулю, сенсор экспортируется в течении LingerTimeout (по умолчанию 5 минут).
+Это сделано для того, чтобы уменьшить число дырок в графиках.
+
+## Global сенсоры
+
+По умолчанию сенсор относится к текущему процессу. При экспорте к нему будет привязан тег `host=`,
+или другой идентификатор текущего процесса.
+
+Сенсор можно сделать "глобальным". Тогда при экспорте к нему не будут добавляться теги процесса. Но
+клиент должен гарантировать, что такой сенсор в один момент времени активен только на одном процессе
+кластера.
+
+## Теги и проекции
+
+Перед экспортом сенсоров в Соломон, YT вычисляет локальные агрегаты.
+
+Все сенсоры с одинаковым именем должны быть проаннотированы тегами с
+одинаковыми ключами. Нарушать это правило можно, но крайне не рекомендуется.
+
+```
+# Неверно. Одинаковое имя, но разные ключи тегов.
+request_count{user=foo} 1
+request_count{command=get} 1
+
+# Правильно
+request_count_by_user{user=foo} 1
+request_count_by_command{command=get} 1
+
+# Правильно
+request_count{user=foo;command=get} 1
+```
+
+По дефолту, агрегация считается по всем подмножествам множества ключей.
+Экспортируются только агрегаты, а не сами сенсоры.
+
+В некоторых ситуациях такое поведение приводит к комбинаторному взрыву
+или бесполезной работе.
+
+Например:
+
+```
+# Исходный сенсор
+request_count{bundle=sys;table_path=//sys/operations}
+
+# Значение агрегата с тегом table_path всегда будет равно
+# исходному сенсору. Такая проекция увеличивает нагрузку на Соломон
+# в 2 раза и не добавляет никакой новой информации.
+request_count{table_path=//sys/operations}
+
+# Агрегат по всем таблицам бандла очень полезен, и его нужно оставить.
+request_count{bundle=sys}
+```
+
+Чтобы избежать этой проблемы, можно исключать некоторые множества ключей
+из агрегации.
+
+- `Required` тег обязан присутствовать в множестве ключей для агрегации.
+- `Excluded` тег исключает все множества ключей, в которых он присутствует.
+- `Alternative(other)` тег исключает все подмножества ключей, где присутствуют одновременно
+ тег и его альтернативный тег.
+- Тег, для которого определён `Parent`, исключает все подмножества, в которых
+ присутствует тег, но не присутствует его родитель.
+
+Пример: Допустим у нас есть счётчик с тегами `a` и `b`.
+Таблица показывает подмножества агрегатов, которые будут экспортироваться,
+в зависимости от настроек тегов.
+
+| Aggregate | A is required | B is excluded | A is parent of B | A is alternative to B |
+|-----------------|---------------|---------------|------------------|-----------------------|
+| `{}` | - | + | + | + |
+| `{a=foo}` | + | + | + | + |
+| `{b=bar}` | - | - | - | + |
+| `{a=foo;b=bar}` | + | - | + | - |
+
+## Агрегаты Соломона
+
+Текущие агрегаты Соломона умеют считать сумму метрик при записи.
+
+Сумма имеет смысл не для всех типов сенсоров. Чтобы не создавать в Соломоне
+бесполезные графики, библиотека помечает все значения, для которых имеет смысл вычислять
+агрегаты, тегом `yt_aggr=1`.
+
+В настройках шарда должено быть включено единственное правило `host=*, yt_aggr=1 -> host=Aggr, yt_aggr=1`.
diff --git a/yt/yt/library/profiling/example/main.cpp b/yt/yt/library/profiling/example/main.cpp
new file mode 100644
index 0000000000..ad16fd7169
--- /dev/null
+++ b/yt/yt/library/profiling/example/main.cpp
@@ -0,0 +1,162 @@
+#include <random>
+
+#include <unistd.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+#include <yt/yt/core/concurrency/action_queue.h>
+
+#include <yt/yt/core/http/server.h>
+
+#include <yt/yt/core/utilex/random.h>
+
+#include <yt/yt/core/misc/ref_counted_tracker_profiler.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/solomon/exporter.h>
+#include <yt/yt/library/profiling/solomon/registry.h>
+#include <yt/yt/library/profiling/tcmalloc/profiler.h>
+#include <yt/yt/library/profiling/perf/counters.h>
+
+#include <util/stream/output.h>
+#include <util/system/compiler.h>
+#include <util/generic/yexception.h>
+#include <util/string/cast.h>
+#include <util/system/madvise.h>
+
+using namespace NYT;
+using namespace NYT::NHttp;
+using namespace NYT::NConcurrency;
+using namespace NYT::NProfiling;
+
+int main(int argc, char* argv[])
+{
+ EnableTCMallocProfiler();
+ EnablePerfCounters();
+
+ try {
+ if (argc != 2 && argc != 3) {
+ throw yexception() << "usage: " << argv[0] << " PORT [--fast]";
+ }
+
+ auto port = FromString<int>(argv[1]);
+ auto fast = TString{"--fast"} == TString{argv[2]};
+ auto poller = CreateThreadPoolPoller(1, "Example");
+ auto server = CreateServer(port, poller);
+ auto actionQueue = New<TActionQueue>("Control");
+
+ auto threadPool = CreateThreadPool(16, "Pool");
+
+ auto internalShardConfig = New<TShardConfig>();
+ internalShardConfig->Filter = {"yt/solomon"};
+
+ auto defaultShardConfig = New<TShardConfig>();
+ defaultShardConfig->Filter = {""};
+
+ auto config = New<TSolomonExporterConfig>();
+ config->Shards = {
+ {"internal", internalShardConfig},
+ {"default", defaultShardConfig},
+ };
+
+ if (fast) {
+ config->GridStep = TDuration::Seconds(2);
+ }
+
+ // Deprecated option. Enabled for testing.
+ config->EnableCoreProfilingCompatibility = true;
+
+ // Offload sensor processing to the thread pool.
+ config->ThreadPoolSize = 16;
+
+ auto exporter = New<TSolomonExporter>(config);
+ exporter->Register("/solomon", server);
+ exporter->Start();
+
+ server->Start();
+
+ EnableRefCountedTrackerProfiling();
+
+ TProfiler r{"/my_loop"};
+
+ auto iterationCount = r.WithTag("thread", "main").Counter("/iteration_count");
+ auto randomNumber = r.WithTag("thread", "main").Gauge("/random_number");
+
+ auto invalidCounter = r.Counter("/invalid");
+ auto invalidGauge = r.Gauge("/invalid");
+
+ auto sparseCounter = r.WithSparse().Counter("/sparse_count");
+
+ auto histogram = r.WithSparse().TimeHistogram(
+ "/histogram",
+ TDuration::MilliSeconds(1),
+ TDuration::Seconds(1));
+
+ auto constHistogram = r.WithSparse().TimeHistogram(
+ "/const_histogram",
+ TDuration::MilliSeconds(1),
+ TDuration::Seconds(1));
+
+ auto poolUsage = r.WithTag("pool", "prime").WithGlobal().Gauge("/cpu");
+ poolUsage.Update(3000.0);
+
+ for (int i = 0; i < 10; i++) {
+ auto remoteRegistry = New<TSolomonRegistry>();
+ auto config = New<TSolomonExporterConfig>();
+ config->EnableSelfProfiling = false;
+ config->ReportRestart = false;
+
+ auto remoteExporter = New<TSolomonExporter>(config, remoteRegistry);
+
+ TProfiler r{remoteRegistry, "/remote"};
+ r.AddFuncGauge("/value", remoteExporter, [] { return 1.0; });
+
+ auto h = r.GaugeHistogram("/hist", {0, 1, 2});
+
+ exporter->AttachRemoteProcess(BIND([remoteExporter] () -> TFuture<TSharedRef> {
+ return MakeFuture(remoteExporter->DumpSensors());
+ }));
+ }
+
+ exporter->AttachRemoteProcess(BIND([] () -> TFuture<TSharedRef> {
+ THROW_ERROR_EXCEPTION("Process is dead");
+ }));
+ exporter->AttachRemoteProcess(BIND([] () -> TFuture<TSharedRef> {
+ return MakeFuture<TSharedRef>(TError("Process is dead"));
+ }));
+
+ std::default_random_engine rng;
+ double value = 0.0;
+
+ auto ptr = malloc(4_GB);
+ for (i64 i = 0; true; ++i) {
+ YT_PROFILE_TIMING("/loop_start") {
+ iterationCount.Increment();
+ randomNumber.Update(value);
+ }
+ value += std::uniform_real_distribution<double>(-1, 1)(rng);
+
+ YT_PROFILE_TIMING("/busy_wait") {
+ // Busy wait to demonstrate CPU tracker.
+ auto endBusyTime = TInstant::Now() + TDuration::MilliSeconds(10);
+ while (TInstant::Now() < endBusyTime)
+ { }
+ }
+
+ histogram.Record(RandomDuration(TDuration::Seconds(1)));
+ constHistogram.Record(TDuration::Seconds(1) / 2);
+
+ if (i % 18000 == 0) {
+ sparseCounter.Increment();
+ }
+
+ *reinterpret_cast<int*>(ptr) = 0;
+ MadviseEvict(ptr, 4096);
+ }
+ } catch (const std::exception& ex) {
+ Cerr << ex.what() << Endl;
+ _exit(1);
+ }
+
+ return 0;
+}
diff --git a/yt/yt/library/profiling/example/ya.make b/yt/yt/library/profiling/example/ya.make
new file mode 100644
index 0000000000..3158197781
--- /dev/null
+++ b/yt/yt/library/profiling/example/ya.make
@@ -0,0 +1,17 @@
+PROGRAM(profiling-example)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(main.cpp)
+
+IF (OS_LINUX)
+ ALLOCATOR(TCMALLOC_256K)
+ENDIF()
+
+PEERDIR(
+ yt/yt/library/profiling/solomon
+ yt/yt/library/profiling/tcmalloc
+ yt/yt/library/profiling/perf
+)
+
+END()
diff --git a/yt/yt/library/profiling/histogram_snapshot.cpp b/yt/yt/library/profiling/histogram_snapshot.cpp
new file mode 100644
index 0000000000..0deb9afa8c
--- /dev/null
+++ b/yt/yt/library/profiling/histogram_snapshot.cpp
@@ -0,0 +1,115 @@
+#include "histogram_snapshot.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+THistogramSnapshot MergeHistograms(const THistogramSnapshot& first, const THistogramSnapshot& second)
+{
+ THistogramSnapshot result;
+
+ size_t i = 0, j = 0;
+ auto pushFirst = [&] {
+ result.Bounds.push_back(first.Bounds[i]);
+ result.Values.push_back(first.Values[i]);
+ ++i;
+ };
+ auto pushSecond = [&] {
+ result.Bounds.push_back(second.Bounds[j]);
+ result.Values.push_back(second.Values[j]);
+ ++j;
+ };
+
+ while (true) {
+ if (i == first.Bounds.size() || j == second.Bounds.size()) {
+ break;
+ }
+
+ if (first.Bounds[i] < second.Bounds[j]) {
+ pushFirst();
+ } else if (first.Bounds[i] > second.Bounds[j]) {
+ pushSecond();
+ } else {
+ result.Bounds.push_back(second.Bounds[j]);
+ result.Values.push_back(first.Values[i] + second.Values[j]);
+
+ ++i;
+ ++j;
+ }
+ }
+
+ while (i != first.Bounds.size()) {
+ pushFirst();
+ }
+
+ while (j != second.Bounds.size()) {
+ pushSecond();
+ }
+
+ int infBucket = 0;
+ if (first.Values.size() != first.Bounds.size()) {
+ infBucket += first.Values.back();
+ }
+ if (second.Values.size() != second.Bounds.size()) {
+ infBucket += second.Values.back();
+ }
+ if (infBucket != 0) {
+ result.Values.push_back(infBucket);
+ }
+
+ return result;
+}
+
+THistogramSnapshot& THistogramSnapshot::operator += (const THistogramSnapshot& other)
+{
+ if (Bounds.empty()) {
+ Bounds = other.Bounds;
+ Values = other.Values;
+ } else if (other.Bounds.empty()) {
+ // Do nothing
+ } else if (Bounds == other.Bounds) {
+ if (Values.size() < other.Values.size()) {
+ Values.push_back(0);
+ YT_VERIFY(Values.size() == other.Values.size());
+ }
+
+ for (size_t i = 0; i < other.Values.size(); ++i) {
+ Values[i] += other.Values[i];
+ }
+ } else {
+ *this = MergeHistograms(*this, other);
+ }
+
+ return *this;
+}
+
+bool THistogramSnapshot::IsEmpty() const
+{
+ bool empty = true;
+ for (auto value : Values) {
+ if (value != 0) {
+ empty = false;
+ }
+ }
+ return empty;
+}
+
+bool THistogramSnapshot::operator == (const THistogramSnapshot& other) const
+{
+ if (IsEmpty() && other.IsEmpty()) {
+ return true;
+ }
+
+ return Values == other.Values && Bounds == other.Bounds;
+}
+
+bool THistogramSnapshot::operator != (const THistogramSnapshot& other) const
+{
+ return !(*this == other);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/histogram_snapshot.h b/yt/yt/library/profiling/histogram_snapshot.h
new file mode 100644
index 0000000000..782215da14
--- /dev/null
+++ b/yt/yt/library/profiling/histogram_snapshot.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <util/datetime/base.h>
+
+#include <vector>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct THistogramSnapshot
+{
+ // When Values.size() == Bounds.size() + 1, Values.back() stores "Inf" bucket.
+ std::vector<int> Values;
+ std::vector<double> Bounds;
+
+ THistogramSnapshot& operator += (const THistogramSnapshot& other);
+
+ bool operator == (const THistogramSnapshot& other) const;
+ bool operator != (const THistogramSnapshot& other) const;
+ bool IsEmpty() const;
+};
+
+struct TTimeHistogramSnapshot
+ : public THistogramSnapshot
+{
+ TTimeHistogramSnapshot() = default;
+
+ TTimeHistogramSnapshot(const THistogramSnapshot& hist)
+ : THistogramSnapshot(hist)
+ { }
+};
+
+struct TGaugeHistogramSnapshot
+ : public THistogramSnapshot
+{
+ TGaugeHistogramSnapshot() = default;
+
+ TGaugeHistogramSnapshot(const THistogramSnapshot& hist)
+ : THistogramSnapshot(hist)
+ { }
+};
+
+struct TRateHistogramSnapshot
+ : public THistogramSnapshot
+{
+ TRateHistogramSnapshot() = default;
+
+ TRateHistogramSnapshot(const THistogramSnapshot& hist)
+ : THistogramSnapshot(hist)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/impl.cpp b/yt/yt/library/profiling/impl.cpp
new file mode 100644
index 0000000000..c77cebf751
--- /dev/null
+++ b/yt/yt/library/profiling/impl.cpp
@@ -0,0 +1,16 @@
+#include "impl.h"
+
+#include <util/system/compiler.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IRegistryImplPtr Y_WEAK GetGlobalRegistry()
+{
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/impl.h b/yt/yt/library/profiling/impl.h
new file mode 100644
index 0000000000..59f0b25ce7
--- /dev/null
+++ b/yt/yt/library/profiling/impl.h
@@ -0,0 +1,180 @@
+#pragma once
+
+#include "public.h"
+#include "sensor.h"
+#include "summary.h"
+
+#include <library/cpp/yt/memory/weak_ptr.h>
+#include <library/cpp/yt/memory/ref_counted.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IRegistryImpl
+ : public TRefCounted
+{
+public:
+ virtual ICounterImplPtr RegisterCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual ITimeCounterImplPtr RegisterTimeCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual IGaugeImplPtr RegisterGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual ITimeGaugeImplPtr RegisterTimeGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual ISummaryImplPtr RegisterSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual IGaugeImplPtr RegisterGaugeSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual ITimeGaugeImplPtr RegisterTimeGaugeSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual ITimerImplPtr RegisterTimerSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual ITimerImplPtr RegisterTimeHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual IHistogramImplPtr RegisterGaugeHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual IHistogramImplPtr RegisterRateHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) = 0;
+
+ virtual void RegisterFuncCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const TRefCountedPtr& owner,
+ std::function<i64()> reader) = 0;
+
+ virtual void RegisterFuncGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const TRefCountedPtr& owner,
+ std::function<double()> reader) = 0;
+
+ virtual void RegisterProducer(
+ const TString& prefix,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const ISensorProducerPtr& owner) = 0;
+
+ virtual void RenameDynamicTag(
+ const TDynamicTagPtr& tag,
+ const TString& name,
+ const TString& value) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IRegistryImpl)
+
+IRegistryImplPtr GetGlobalRegistry();
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ICounterImpl
+ : public TRefCounted
+{
+ virtual void Increment(i64 delta) = 0;
+ virtual i64 GetValue() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ICounterImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ITimeCounterImpl
+ : public TRefCounted
+{
+ virtual void Add(TDuration delta) = 0;
+
+ virtual TDuration GetValue() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITimeCounterImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IGaugeImpl
+ : public virtual TRefCounted
+{
+ virtual void Update(double value) = 0;
+ virtual double GetValue() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IGaugeImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ITimeGaugeImpl
+ : public virtual TRefCounted
+{
+ virtual void Update(TDuration value) = 0;
+ virtual TDuration GetValue() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITimeGaugeImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct ISummaryImplBase
+ : public virtual TRefCounted
+{
+ virtual void Record(T value) = 0;
+
+ virtual TSummarySnapshot<T> GetSummary() = 0;
+ virtual TSummarySnapshot<T> GetSummaryAndReset() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ISummaryImpl)
+DEFINE_REFCOUNTED_TYPE(ITimerImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IHistogramImpl
+ : public virtual TRefCounted
+{
+ virtual void Add(double value, int count) = 0;
+ virtual void Remove(double value, int count) = 0;
+ virtual void Reset() = 0;
+
+ virtual THistogramSnapshot GetSnapshot(bool reset) = 0;
+ virtual void LoadSnapshot(THistogramSnapshot snapshot) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IHistogramImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/integration/test_solomon.py b/yt/yt/library/profiling/integration/test_solomon.py
new file mode 100644
index 0000000000..c40ba84931
--- /dev/null
+++ b/yt/yt/library/profiling/integration/test_solomon.py
@@ -0,0 +1,103 @@
+import time
+import pytest
+import requests
+import datetime
+
+import yatest.common
+
+from yatest.common import network
+
+
+@pytest.fixture(scope="session")
+def test_url():
+ with network.PortManager() as pm:
+ port = pm.get_port()
+
+ cmd = [
+ yatest.common.binary_path("yt/yt/library/profiling/example/profiling-example"),
+ str(port),
+ "--fast"
+ ]
+
+ p = yatest.common.execute(cmd, wait=False, env={"YT_LOG_LEVEL": "DEBUG"})
+ time.sleep(20)
+ assert p.running
+
+ yield f"http://localhost:{port}"
+
+
+def check_ok(url, headers={}):
+ req = requests.get(url, headers=headers)
+ req.raise_for_status()
+
+
+def test_debug(test_url):
+ check_ok(test_url + "/solomon/")
+ check_ok(test_url + "/solomon/tags")
+ check_ok(test_url + "/solomon/sensors")
+ check_ok(test_url + "/solomon/status")
+
+ status = requests.get(test_url + "/solomon/status").json()
+ assert "yt/my_loop/invalid" in status["sensor_errors"]
+ assert len(status["window"]) > 5
+
+
+@pytest.mark.parametrize('format', ["application/json", "application/x-spack"])
+def test_get_last(test_url, format):
+ check_ok(test_url + "/solomon/all", headers={"Accept": format})
+ check_ok(test_url + "/solomon/shard/internal", headers={"Accept": format})
+ check_ok(test_url + "/solomon/shard/default", headers={"Accept": format})
+
+
+def format_now(delta=None, grid=2):
+ d = datetime.datetime.utcnow()
+ if d.second % grid != 0:
+ d -= datetime.timedelta(seconds=d.second % grid)
+
+ if delta:
+ d += delta
+
+ # 2020-11-10T17:21:00.000000
+ return d.strftime("%Y-%m-%dT%H:%M:%S")
+
+
+@pytest.mark.parametrize('format', ["application/json", "application/x-spack"])
+def test_timestamp_args(test_url, format):
+ now = format_now()
+ check_ok(test_url + f"/solomon/all?now={now}&period=10s", headers={"Accept": format})
+
+ # test reply cache
+ for i in range(10):
+ check_ok(test_url + f"/solomon/all?now={now}&period=6s", headers={"Accept": format})
+
+
+@pytest.mark.parametrize('format', ["application/json", "application/x-spack"])
+def test_timestamp_grid(test_url, format):
+ now = format_now()
+
+ check_ok(test_url + f"/solomon/all?now={now}&period=10s", headers={"Accept": format, "X-Solomon-GridSec": "2"})
+ check_ok(test_url + f"/solomon/all?now={now}&period=10s", headers={"Accept": format, "X-Solomon-GridSec": "10"})
+
+
+def check_not_ok(url, headers={}):
+ req = requests.get(url, headers=headers)
+ assert req.status_code != 200
+
+
+def test_invalid(test_url):
+ now = format_now()
+ unaligned_now = format_now(datetime.timedelta(seconds=1))
+ future = format_now(datetime.timedelta(hours=6))
+ past = format_now(datetime.timedelta(hours=-6))
+
+ check_not_ok(test_url + f"/solomon/all?now={now}")
+ check_not_ok(test_url + f"/solomon/all?now={unaligned_now}")
+ check_not_ok(test_url + f"/solomon/all?now={now}&period=180s")
+ check_not_ok(test_url + f"/solomon/all?now={past}&period=10s")
+ check_not_ok(test_url + f"/solomon/all?now={future}&period=10s")
+
+ check_not_ok(test_url + f"/solomon/all?now={now}&period=0s")
+ check_not_ok(test_url + f"/solomon/all?now={now}&period=3s")
+ check_not_ok(test_url + f"/solomon/all?now={now}&period=10s", headers={"X-Solomon-GridSec": "3"})
+ check_not_ok(test_url + f"/solomon/all?now={now}&period=10s", headers={"X-Solomon-GridSec": "20"})
+ check_not_ok(test_url + f"/solomon/all?now={now}&period=10s", headers={"X-Solomon-GridSec": "0"})
diff --git a/yt/yt/library/profiling/integration/ya.make b/yt/yt/library/profiling/integration/ya.make
new file mode 100644
index 0000000000..f7e07eeab6
--- /dev/null
+++ b/yt/yt/library/profiling/integration/ya.make
@@ -0,0 +1,17 @@
+PY3TEST()
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ contrib/python/requests
+)
+
+TEST_SRCS(
+ test_solomon.py
+)
+
+DEPENDS(
+ yt/yt/library/profiling/example
+)
+
+END()
diff --git a/yt/yt/library/profiling/perf/counters.cpp b/yt/yt/library/profiling/perf/counters.cpp
new file mode 100644
index 0000000000..e9f2ee2c61
--- /dev/null
+++ b/yt/yt/library/profiling/perf/counters.cpp
@@ -0,0 +1,250 @@
+#include "counters.h"
+
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <linux/perf_event.h>
+
+#include <sys/syscall.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+struct TPerfEventDescription final
+{
+ int EventType;
+ int EventConfig;
+};
+
+constexpr TPerfEventDescription SoftwareEvent(int perfName) noexcept
+{
+ return {PERF_TYPE_SOFTWARE, perfName};
+}
+
+constexpr TPerfEventDescription HardwareEvent(int perfName) noexcept
+{
+ return {PERF_TYPE_HARDWARE, perfName};
+}
+
+enum class ECacheEventType
+{
+ Access,
+ Miss,
+};
+
+enum class ECacheActionType
+{
+ Load,
+ Store,
+};
+
+TPerfEventDescription CacheEvent(
+ int perfName,
+ ECacheActionType eventType,
+ ECacheEventType eventResultType) noexcept
+{
+ constexpr auto kEventNameShift = 0;
+ constexpr auto kCacheActionTypeShift = 8;
+ constexpr auto kEventTypeShift = 16;
+
+ int cacheActionTypeForConfig = [&] {
+ switch (eventType) {
+ case ECacheActionType::Load:
+ return PERF_COUNT_HW_CACHE_OP_READ;
+ case ECacheActionType::Store:
+ return PERF_COUNT_HW_CACHE_OP_WRITE;
+ default:
+ YT_ABORT();
+ }
+ }();
+
+ int eventTypeForConfig = [&] {
+ switch (eventResultType) {
+ case ECacheEventType::Access:
+ return PERF_COUNT_HW_CACHE_RESULT_ACCESS;
+ case ECacheEventType::Miss:
+ return PERF_COUNT_HW_CACHE_RESULT_MISS;
+ default:
+ YT_ABORT();
+ }
+ }();
+
+ int eventConfig = (perfName << kEventNameShift) |
+ (cacheActionTypeForConfig << kCacheActionTypeShift) |
+ (eventTypeForConfig << kEventTypeShift);
+
+ return {PERF_TYPE_HW_CACHE, eventConfig};
+}
+
+TPerfEventDescription GetDescription(EPerfEventType type)
+{
+ switch (type) {
+ case EPerfEventType::CpuCycles:
+ return HardwareEvent(PERF_COUNT_HW_CPU_CYCLES);
+ case EPerfEventType::Instructions:
+ return HardwareEvent(PERF_COUNT_HW_INSTRUCTIONS);
+ case EPerfEventType::CacheReferences:
+ return HardwareEvent(PERF_COUNT_HW_CACHE_REFERENCES);
+ case EPerfEventType::CacheMisses:
+ return HardwareEvent(PERF_COUNT_HW_CACHE_MISSES);
+ case EPerfEventType::BranchInstructions:
+ return HardwareEvent(PERF_COUNT_HW_BRANCH_INSTRUCTIONS);
+ case EPerfEventType::BranchMisses:
+ return HardwareEvent(PERF_COUNT_HW_BRANCH_MISSES);
+ case EPerfEventType::BusCycles:
+ return HardwareEvent(PERF_COUNT_HW_BUS_CYCLES);
+ case EPerfEventType::StalledCyclesFrontend:
+ return HardwareEvent(PERF_COUNT_HW_STALLED_CYCLES_FRONTEND);
+ case EPerfEventType::StalledCyclesBackend:
+ return HardwareEvent(PERF_COUNT_HW_STALLED_CYCLES_BACKEND);
+ case EPerfEventType::RefCpuCycles:
+ return HardwareEvent(PERF_COUNT_HW_REF_CPU_CYCLES);
+
+
+ case EPerfEventType::PageFaults:
+ return SoftwareEvent(PERF_COUNT_SW_PAGE_FAULTS);
+ case EPerfEventType::MinorPageFaults:
+ return SoftwareEvent(PERF_COUNT_SW_PAGE_FAULTS_MIN);
+ case EPerfEventType::MajorPageFaults:
+ return SoftwareEvent(PERF_COUNT_SW_PAGE_FAULTS_MAJ);
+
+ // `cpu-clock` is a bit broken according to this: https://stackoverflow.com/a/56967896
+ case EPerfEventType::CpuClock:
+ return SoftwareEvent(PERF_COUNT_SW_CPU_CLOCK);
+ case EPerfEventType::TaskClock:
+ return SoftwareEvent(PERF_COUNT_SW_TASK_CLOCK);
+ case EPerfEventType::ContextSwitches:
+ return SoftwareEvent(PERF_COUNT_SW_CONTEXT_SWITCHES);
+ case EPerfEventType::CpuMigrations:
+ return SoftwareEvent(PERF_COUNT_SW_CPU_MIGRATIONS);
+ case EPerfEventType::AlignmentFaults:
+ return SoftwareEvent(PERF_COUNT_SW_ALIGNMENT_FAULTS);
+ case EPerfEventType::EmulationFaults:
+ return SoftwareEvent(PERF_COUNT_SW_EMULATION_FAULTS);
+ case EPerfEventType::DataTlbReferences:
+ return CacheEvent(PERF_COUNT_HW_CACHE_DTLB, ECacheActionType::Load, ECacheEventType::Access);
+ case EPerfEventType::DataTlbMisses:
+ return CacheEvent(PERF_COUNT_HW_CACHE_DTLB, ECacheActionType::Load, ECacheEventType::Miss);
+ case EPerfEventType::DataStoreTlbReferences:
+ return CacheEvent(PERF_COUNT_HW_CACHE_DTLB, ECacheActionType::Store, ECacheEventType::Access);
+ case EPerfEventType::DataStoreTlbMisses:
+ return CacheEvent(PERF_COUNT_HW_CACHE_DTLB, ECacheActionType::Store, ECacheEventType::Miss);
+
+ // Apparently it doesn't make sense to treat these values as relative:
+ // https://stackoverflow.com/questions/49933319/how-to-interpret-perf-itlb-loads-itlb-load-misses
+ case EPerfEventType::InstructionTlbReferences:
+ return CacheEvent(PERF_COUNT_HW_CACHE_ITLB, ECacheActionType::Load, ECacheEventType::Access);
+ case EPerfEventType::InstructionTlbMisses:
+ return CacheEvent(PERF_COUNT_HW_CACHE_ITLB, ECacheActionType::Load, ECacheEventType::Miss);
+ case EPerfEventType::LocalMemoryReferences:
+ return CacheEvent(PERF_COUNT_HW_CACHE_NODE, ECacheActionType::Load, ECacheEventType::Access);
+ case EPerfEventType::LocalMemoryMisses:
+ return CacheEvent(PERF_COUNT_HW_CACHE_NODE, ECacheActionType::Load, ECacheEventType::Miss);
+
+ default:
+ YT_ABORT();
+ }
+}
+
+int OpenPerfEvent(int tid, int eventType, int eventConfig)
+{
+ perf_event_attr attr{};
+
+ attr.type = eventType;
+ attr.size = sizeof(attr);
+ attr.config = eventConfig;
+ attr.inherit = 1;
+
+ int fd = HandleEintr(syscall, SYS_perf_event_open, &attr, tid, -1, -1, PERF_FLAG_FD_CLOEXEC);
+ if (fd == -1) {
+ THROW_ERROR_EXCEPTION("Failed to open perf event descriptor")
+ << TError::FromSystem()
+ << TErrorAttribute("type", eventType)
+ << TErrorAttribute("config", eventConfig);
+ }
+ return fd;
+}
+
+ui64 FetchPerfCounter(int fd)
+{
+ ui64 num{};
+ if (HandleEintr(read, fd, &num, sizeof(num)) != sizeof(num)) {
+ THROW_ERROR_EXCEPTION("Failed to read perf event counter")
+ << TError::FromSystem();
+ }
+ return num;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPerfEventCounter::TPerfEventCounter(EPerfEventType type)
+ : FD_(OpenPerfEvent(
+ 0,
+ GetDescription(type).EventType,
+ GetDescription(type).EventConfig))
+{ }
+
+TPerfEventCounter::~TPerfEventCounter()
+{
+ if (FD_ != -1) {
+ SafeClose(FD_, false);
+ }
+}
+
+ui64 TPerfEventCounter::Read()
+{
+ return FetchPerfCounter(FD_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TCounterOwner)
+
+struct TCounterOwner
+ : public TRefCounted
+{
+ std::vector<std::unique_ptr<TPerfEventCounter>> Counters;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCounterOwner)
+
+void EnablePerfCounters()
+{
+ auto owner = LeakyRefCountedSingleton<TCounterOwner>();
+
+ auto exportCounter = [&] (const TString& category, const TString& name, EPerfEventType type) {
+ try {
+ owner->Counters.emplace_back(new TPerfEventCounter{type});
+ TProfiler{category}.AddFuncCounter(name, owner, [counter=owner->Counters.back().get()] () {
+ return counter->Read();
+ });
+ } catch (const std::exception&) {
+ }
+ };
+
+ exportCounter("/cpu/perf", "/cycles", EPerfEventType::CpuCycles);
+ exportCounter("/cpu/perf", "/instructions", EPerfEventType::Instructions);
+ exportCounter("/cpu/perf", "/branch_instructions", EPerfEventType::BranchInstructions);
+ exportCounter("/cpu/perf", "/branch_misses", EPerfEventType::BranchMisses);
+ exportCounter("/cpu/perf", "/context_switches", EPerfEventType::ContextSwitches);
+
+ exportCounter("/memory/perf", "/page_faults", EPerfEventType::PageFaults);
+ exportCounter("/memory/perf", "/minor_page_faults", EPerfEventType::MinorPageFaults);
+ exportCounter("/memory/perf", "/major_page_faults", EPerfEventType::MajorPageFaults);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/perf/counters.h b/yt/yt/library/profiling/perf/counters.h
new file mode 100644
index 0000000000..6c9f050d9b
--- /dev/null
+++ b/yt/yt/library/profiling/perf/counters.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EPerfEventType,
+ (CpuCycles)
+ (Instructions)
+ (CacheReferences)
+ (CacheMisses)
+ (BranchInstructions)
+ (BranchMisses)
+ (BusCycles)
+ (StalledCyclesFrontend)
+ (StalledCyclesBackend)
+ (RefCpuCycles)
+ (CpuClock)
+ (TaskClock)
+ (ContextSwitches)
+ (PageFaults)
+ (MinorPageFaults)
+ (MajorPageFaults)
+ (CpuMigrations)
+ (AlignmentFaults)
+ (EmulationFaults)
+ (DataTlbReferences)
+ (DataTlbMisses)
+ (DataStoreTlbReferences)
+ (DataStoreTlbMisses)
+ (InstructionTlbReferences)
+ (InstructionTlbMisses)
+ (LocalMemoryReferences)
+ (LocalMemoryMisses)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPerfEventCounter final
+{
+public:
+ explicit TPerfEventCounter(EPerfEventType type);
+ ~TPerfEventCounter();
+
+ TPerfEventCounter(const TPerfEventCounter&) = delete;
+
+ ui64 Read();
+
+private:
+ int FD_ = -1;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! EnablePerfCounters creates selected set of perf counters.
+void EnablePerfCounters();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/perf/counters_other.cpp b/yt/yt/library/profiling/perf/counters_other.cpp
new file mode 100644
index 0000000000..190bcc0c6f
--- /dev/null
+++ b/yt/yt/library/profiling/perf/counters_other.cpp
@@ -0,0 +1,27 @@
+#include "counters.h"
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPerfEventCounter::TPerfEventCounter(EPerfEventType /* type */)
+{
+ Y_UNUSED(FD_);
+}
+
+TPerfEventCounter::~TPerfEventCounter()
+{ }
+
+ui64 TPerfEventCounter::Read()
+{
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void EnablePerfCounters()
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/perf/ya.make b/yt/yt/library/profiling/perf/ya.make
new file mode 100644
index 0000000000..0bf244fdeb
--- /dev/null
+++ b/yt/yt/library/profiling/perf/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (OS_LINUX)
+ SRCS(counters.cpp)
+ELSE()
+ SRCS(counters_other.cpp)
+ENDIF()
+
+PEERDIR(
+ yt/yt/library/profiling
+ yt/yt/core
+)
+
+END()
diff --git a/yt/yt/library/profiling/producer.cpp b/yt/yt/library/profiling/producer.cpp
new file mode 100644
index 0000000000..a224837c06
--- /dev/null
+++ b/yt/yt/library/profiling/producer.cpp
@@ -0,0 +1,152 @@
+#include "producer.h"
+
+#include <util/system/compiler.h>
+
+#include <library/cpp/yt/memory/new.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWithTagGuard::TWithTagGuard(ISensorWriter* writer)
+ : Writer_(writer)
+{
+ YT_VERIFY(Writer_);
+}
+
+TWithTagGuard::TWithTagGuard(ISensorWriter* writer, TString tagKey, TString tagValue)
+ : TWithTagGuard(writer)
+{
+ AddTag(std::move(tagKey), std::move(tagValue));
+}
+
+void TWithTagGuard::AddTag(TTag tag)
+{
+ Writer_->PushTag(std::move(tag));
+ ++AddedTagCount_;
+}
+
+void TWithTagGuard::AddTag(TString tagKey, TString tagValue)
+{
+ AddTag({std::move(tagKey), std::move(tagValue)});
+}
+
+TWithTagGuard::~TWithTagGuard()
+{
+ for (int i = 0; i < AddedTagCount_; ++i) {
+ Writer_->PopTag();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSensorBuffer::PushTag(TTag tag)
+{
+ Tags_.push_back(std::move(tag));
+}
+
+void TSensorBuffer::PopTag()
+{
+ Tags_.pop_back();
+}
+
+void TSensorBuffer::AddGauge(const TString& name, double value)
+{
+ Gauges_.emplace_back(name, Tags_, value);
+}
+
+void TSensorBuffer::AddCounter(const TString& name, i64 value)
+{
+ Counters_.emplace_back(name, Tags_, value);
+}
+
+const std::vector<std::tuple<TString, TTagList, i64>>& TSensorBuffer::GetCounters() const
+{
+ return Counters_;
+}
+
+const std::vector<std::tuple<TString, TTagList, double>>& TSensorBuffer::GetGauges() const
+{
+ return Gauges_;
+}
+
+void TSensorBuffer::WriteTo(ISensorWriter* writer)
+{
+ for (const auto& [name, tags, value] : Counters_) {
+ for (const auto& tag : tags) {
+ writer->PushTag(tag);
+ }
+
+ writer->AddCounter(name, value);
+
+ for (size_t i = 0; i < tags.size(); i++) {
+ writer->PopTag();
+ }
+ }
+
+ for (const auto& [name, tags, value] : Gauges_) {
+ for (const auto& tag : tags) {
+ writer->PushTag(tag);
+ }
+
+ writer->AddGauge(name, value);
+
+ for (size_t i = 0; i < tags.size(); i++) {
+ writer->PopTag();
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TIntrusivePtr<TSensorBuffer> ISensorProducer::GetBuffer()
+{
+ auto buffer = New<TSensorBuffer>();
+ CollectSensors(buffer.Get());
+ return buffer;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBufferedProducer::CollectSensors(ISensorWriter* )
+{
+ YT_ABORT();
+}
+
+TIntrusivePtr<TSensorBuffer> TBufferedProducer::GetBuffer()
+{
+ auto guard = Guard(Lock_);
+ if (Enabled_) {
+ if (Buffer_) {
+ return Buffer_;
+ } else {
+ return New<TSensorBuffer>();
+ }
+ }
+
+ return nullptr;
+}
+
+void TBufferedProducer::SetEnabled(bool enabled)
+{
+ auto guard = Guard(Lock_);
+ Enabled_ = enabled;
+}
+
+void TBufferedProducer::Update(TSensorBuffer buffer)
+{
+ auto ptr = New<TSensorBuffer>(std::move(buffer));
+ auto guard = Guard(Lock_);
+ Buffer_ = ptr;
+}
+
+void TBufferedProducer::Update(const std::function<void(ISensorWriter*)>& callback)
+{
+ TSensorBuffer buffer;
+ callback(&buffer);
+ Update(std::move(buffer));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/producer.h b/yt/yt/library/profiling/producer.h
new file mode 100644
index 0000000000..1212a8f185
--- /dev/null
+++ b/yt/yt/library/profiling/producer.h
@@ -0,0 +1,122 @@
+#pragma once
+
+#include "sensor.h"
+
+#include <util/generic/string.h>
+
+#include <library/cpp/yt/memory/ref_counted.h>
+#include <library/cpp/yt/memory/intrusive_ptr.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISensorWriter
+{
+ virtual ~ISensorWriter() = default;
+
+ virtual void PushTag(TTag tag) = 0;
+ virtual void PopTag() = 0;
+
+ virtual void AddGauge(const TString& name, double value) = 0;
+
+ //! AddCounter emits single counter value.
+ /*!
+ * #value MUST be monotonically increasing.
+ */
+ virtual void AddCounter(const TString& name, i64 value) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! RAII guard for ISensorWriter::PushTag/ISensorWriter::PopTag.
+class TWithTagGuard
+{
+public:
+ TWithTagGuard(const TWithTagGuard&) = delete;
+ TWithTagGuard(TWithTagGuard&&) = delete;
+
+ explicit TWithTagGuard(ISensorWriter* writer);
+ // NB: For convenience.
+ TWithTagGuard(ISensorWriter* writer, TString tagKey, TString tagValue);
+
+ ~TWithTagGuard();
+
+ void AddTag(TTag tag);
+ void AddTag(TString tagKey, TString tagValue);
+
+private:
+ ISensorWriter* const Writer_;
+ int AddedTagCount_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSensorBuffer final
+ : public ISensorWriter
+{
+public:
+ void PushTag(TTag tag) override;
+ void PopTag() override;
+
+ void AddGauge(const TString& name, double value) override;
+ void AddCounter(const TString& name, i64 value) override;
+
+ void WriteTo(ISensorWriter* writer);
+
+ const std::vector<std::tuple<TString, TTagList, i64>>& GetCounters() const;
+ const std::vector<std::tuple<TString, TTagList, double>>& GetGauges() const;
+
+private:
+ TTagList Tags_;
+
+ std::vector<std::tuple<TString, TTagList, i64>> Counters_;
+ std::vector<std::tuple<TString, TTagList, double>> Gauges_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISensorProducer
+ : virtual public TRefCounted
+{
+ //! Collect returns set of gauges or counters associated with this producer.
+ /*!
+ * Registry keeps track of all (name, tags) pair that were ever returned from
+ * this producer.
+ *
+ * Do not use this interface, if set of tags might grow unbound. There is
+ * no way to cleanup removed tags.
+ */
+ virtual void CollectSensors(ISensorWriter* writer) = 0;
+ virtual TIntrusivePtr<TSensorBuffer> GetBuffer();
+};
+
+DEFINE_REFCOUNTED_TYPE(ISensorProducer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBufferedProducer
+ : public ISensorProducer
+{
+public:
+ void CollectSensors(ISensorWriter* writer) override;
+ TIntrusivePtr<TSensorBuffer> GetBuffer() override;
+
+ void Update(TSensorBuffer buffer);
+ void Update(const std::function<void(ISensorWriter*)>& callback);
+
+ void SetEnabled(bool enabled);
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ bool Enabled_ = true;
+ TIntrusivePtr<TSensorBuffer> Buffer_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TBufferedProducer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/public.h b/yt/yt/library/profiling/public.h
new file mode 100644
index 0000000000..db42520669
--- /dev/null
+++ b/yt/yt/library/profiling/public.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <library/cpp/yt/memory/ref_counted.h>
+
+#include <util/datetime/base.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct ISummaryImplBase;
+
+using ISummaryImpl = ISummaryImplBase<double>;
+using ITimerImpl = ISummaryImplBase<TDuration>;
+
+DECLARE_REFCOUNTED_TYPE(ISummaryImpl)
+DECLARE_REFCOUNTED_TYPE(ITimerImpl)
+
+DECLARE_REFCOUNTED_STRUCT(ICounterImpl)
+DECLARE_REFCOUNTED_STRUCT(ITimeCounterImpl)
+DECLARE_REFCOUNTED_STRUCT(IGaugeImpl)
+DECLARE_REFCOUNTED_STRUCT(ITimeGaugeImpl)
+DECLARE_REFCOUNTED_STRUCT(IHistogramImpl)
+DECLARE_REFCOUNTED_STRUCT(IRegistryImpl)
+DECLARE_REFCOUNTED_STRUCT(ISensorProducer)
+DECLARE_REFCOUNTED_CLASS(TBufferedProducer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/resource_tracker/CMakeLists.darwin-x86_64.txt b/yt/yt/library/profiling/resource_tracker/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..fb8973d492
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-profiling-resource_tracker)
+target_compile_options(library-profiling-resource_tracker PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(library-profiling-resource_tracker PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-library-profiling
+ yt_proto-yt-core
+)
+target_sources(library-profiling-resource_tracker PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/resource_tracker/resource_tracker.cpp
+)
diff --git a/yt/yt/library/profiling/resource_tracker/CMakeLists.linux-aarch64.txt b/yt/yt/library/profiling/resource_tracker/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..dfaca6c9b5
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-profiling-resource_tracker)
+target_compile_options(library-profiling-resource_tracker PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(library-profiling-resource_tracker PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-library-profiling
+ yt_proto-yt-core
+)
+target_sources(library-profiling-resource_tracker PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/resource_tracker/resource_tracker.cpp
+)
diff --git a/yt/yt/library/profiling/resource_tracker/CMakeLists.linux-x86_64.txt b/yt/yt/library/profiling/resource_tracker/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..dfaca6c9b5
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,23 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-profiling-resource_tracker)
+target_compile_options(library-profiling-resource_tracker PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(library-profiling-resource_tracker PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-library-profiling
+ yt_proto-yt-core
+)
+target_sources(library-profiling-resource_tracker PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/resource_tracker/resource_tracker.cpp
+)
diff --git a/yt/yt/library/profiling/resource_tracker/CMakeLists.txt b/yt/yt/library/profiling/resource_tracker/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/library/profiling/resource_tracker/CMakeLists.windows-x86_64.txt b/yt/yt/library/profiling/resource_tracker/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..9e7771c8f5
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,19 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-profiling-resource_tracker)
+target_link_libraries(library-profiling-resource_tracker PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-library-profiling
+ yt_proto-yt-core
+)
+target_sources(library-profiling-resource_tracker PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/profiling/resource_tracker/resource_tracker.cpp
+)
diff --git a/yt/yt/library/profiling/resource_tracker/resource_tracker.cpp b/yt/yt/library/profiling/resource_tracker/resource_tracker.cpp
new file mode 100644
index 0000000000..88f5c91a8a
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/resource_tracker.cpp
@@ -0,0 +1,444 @@
+#include "resource_tracker.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/fs.h>
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <util/folder/filelist.h>
+
+#include <util/stream/file.h>
+
+#include <util/string/vector.h>
+
+#if defined(_linux_)
+#include <unistd.h>
+#endif
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYPath;
+using namespace NYTree;
+using namespace NProfiling;
+using namespace NConcurrency;
+
+DEFINE_REFCOUNTED_TYPE(TCpuCgroupTracker)
+DEFINE_REFCOUNTED_TYPE(TMemoryCgroupTracker)
+DEFINE_REFCOUNTED_TYPE(TResourceTracker)
+
+////////////////////////////////////////////////////////////////////////////////
+
+static NLogging::TLogger Logger("Profiling");
+static TProfiler Profiler("/resource_tracker");
+static TProfiler MemoryProfiler("/memory/cgroup");
+
+// Please, refer to /proc documentation to know more about available information.
+// http://www.kernel.org/doc/Documentation/filesystems/proc.txt
+static constexpr auto procPath = "/proc/self/task";
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+i64 GetTicksPerSecond()
+{
+#if defined(_linux_)
+ return sysconf(_SC_CLK_TCK);
+#else
+ return -1;
+#endif
+}
+
+#ifdef RESOURCE_TRACKER_ENABLED
+
+
+#endif
+
+} // namespace
+
+void TCpuCgroupTracker::CollectSensors(ISensorWriter* writer)
+{
+ try {
+ auto cgroups = GetProcessCgroups();
+ for (const auto& group : cgroups) {
+ for (const auto& controller : group.Controllers) {
+ if (controller == "cpu") {
+ auto stat = GetCgroupCpuStat(group.ControllersName, group.Path);
+
+ if (!FirstCgroupStat_) {
+ FirstCgroupStat_ = stat;
+ }
+
+ writer->AddCounter(
+ "/cgroup/periods",
+ stat.NrPeriods - FirstCgroupStat_->NrPeriods);
+
+ writer->AddCounter(
+ "/cgroup/throttled_periods",
+ stat.NrThrottled - FirstCgroupStat_->NrThrottled);
+
+ writer->AddCounter(
+ "/cgroup/throttled_cpu_percent",
+ (stat.ThrottledTime - FirstCgroupStat_->ThrottledTime) / 10'000'000);
+
+ writer->AddCounter(
+ "/cgroup/wait_cpu_percent",
+ (stat.WaitTime - FirstCgroupStat_->WaitTime) / 10'000'000);
+
+ return;
+ }
+ }
+ }
+ } catch (const std::exception& ex) {
+ if (!CgroupErrorLogged_) {
+ YT_LOG_INFO(ex, "Failed to collect cgroup cpu statistics");
+ CgroupErrorLogged_ = true;
+ }
+ }
+}
+
+void TMemoryCgroupTracker::CollectSensors(ISensorWriter* writer)
+{
+ try {
+ auto cgroups = GetProcessCgroups();
+ for (const auto& group : cgroups) {
+ for (const auto& controller : group.Controllers) {
+ if (controller == "memory") {
+ auto stat = GetCgroupMemoryStat(group.Path);
+
+ writer->AddGauge("/memory_limit", stat.HierarchicalMemoryLimit);
+ writer->AddGauge("/cache", stat.Cache);
+ writer->AddGauge("/rss", stat.Rss);
+ writer->AddGauge("/rss_huge", stat.RssHuge);
+ writer->AddGauge("/mapped_file", stat.MappedFile);
+ writer->AddGauge("/dirty", stat.Dirty);
+ writer->AddGauge("/writeback", stat.Writeback);
+
+ TotalMemoryLimit.store(stat.HierarchicalMemoryLimit);
+
+ return;
+ }
+ }
+ }
+ } catch (const std::exception& ex) {
+ if (!CgroupErrorLogged_) {
+ YT_LOG_INFO(ex, "Failed to collect cgroup memory statistics");
+ CgroupErrorLogged_ = true;
+ }
+ }
+}
+
+i64 TMemoryCgroupTracker::GetTotalMemoryLimit()
+{
+ return TotalMemoryLimit.load();
+}
+
+TResourceTracker::TTimings TResourceTracker::TTimings::operator-(const TResourceTracker::TTimings& other) const
+{
+ return {UserJiffies - other.UserJiffies, SystemJiffies - other.SystemJiffies, CpuWaitNsec - other.CpuWaitNsec};
+}
+
+TResourceTracker::TTimings& TResourceTracker::TTimings::operator+=(const TResourceTracker::TTimings& other)
+{
+ UserJiffies += other.UserJiffies;
+ SystemJiffies += other.SystemJiffies;
+ CpuWaitNsec += other.CpuWaitNsec;
+ return *this;
+}
+
+TResourceTracker::TResourceTracker()
+ // CPU time is measured in jiffies; we need USER_HZ to convert them
+ // to milliseconds and percentages.
+ : TicksPerSecond_(GetTicksPerSecond())
+ , LastUpdateTime_(TInstant::Now())
+ , CpuCgroupTracker_(New<TCpuCgroupTracker>())
+ , MemoryCgroupTracker_(New<TMemoryCgroupTracker>())
+{
+ Profiler.AddFuncGauge("/memory_usage/rss", MakeStrong(this), [] {
+ return GetProcessMemoryUsage().Rss;
+ });
+
+ Profiler.AddFuncGauge("/utilization/max", MakeStrong(this), [this] {
+ return MaxThreadPoolUtilization_.load();
+ });
+
+ Profiler.AddProducer("", CpuCgroupTracker_);
+ MemoryProfiler.AddProducer("", MemoryCgroupTracker_);
+
+ Profiler
+ .WithProducerRemoveSupport()
+ .AddProducer("", MakeStrong(this));
+}
+
+void TResourceTracker::CollectSensors(ISensorWriter* writer)
+{
+ i64 timeDeltaUsec = TInstant::Now().MicroSeconds() - LastUpdateTime_.MicroSeconds();
+ if (timeDeltaUsec <= 0) {
+ return;
+ }
+
+ auto tidToInfo = ProcessThreads();
+ CollectSensorsAggregatedTimings(writer, TidToInfo_, tidToInfo, timeDeltaUsec);
+ TidToInfo_ = tidToInfo;
+
+ LastUpdateTime_ = TInstant::Now();
+}
+
+void TResourceTracker::SetVCpuFactor(double vCpuFactor)
+{
+ VCpuFactor_ = vCpuFactor;
+}
+
+bool TResourceTracker::ProcessThread(TString tid, TResourceTracker::TThreadInfo* info)
+{
+ auto threadStatPath = NFS::CombinePaths(procPath, tid);
+ auto statPath = NFS::CombinePaths(threadStatPath, "stat");
+ auto statusPath = NFS::CombinePaths(threadStatPath, "status");
+ auto schedStatPath = NFS::CombinePaths(threadStatPath, "schedstat");
+
+ try {
+ // Parse status.
+ {
+ TIFStream file(statusPath);
+ for (TString line; file.ReadLine(line); ) {
+ auto tokens = SplitString(line, "\t");
+
+ if (tokens.size() < 2) {
+ continue;
+ }
+
+ if (tokens[0] == "Name:") {
+ info->ThreadName = tokens[1];
+ } else if (tokens[0] == "SigBlk:") {
+ // This is a heuristic way to distinguish YT thread from non-YT threads.
+ // It is used primarily for CHYT, which links against CH and Poco that
+ // have their own complicated manner of handling threads. We want to be
+ // able to visually distinguish them from our threads.
+ //
+ // Poco threads always block SIGQUIT, SIGPIPE and SIGTERM; we use the latter
+ // one presence. Feel free to change this heuristic if needed.
+ YT_VERIFY(tokens[1].size() == 16);
+ auto mask = IntFromString<ui64, 16>(tokens[1]);
+ // Note that signals are 1-based, so 14-th bit is SIGTERM (15).
+ bool sigtermBlocked = (mask >> 14) & 1ull;
+ info->IsYtThread = !sigtermBlocked;
+ }
+ }
+ }
+
+ // Parse schedstat.
+ {
+ TIFStream file(schedStatPath);
+ auto tokens = SplitString(file.ReadLine(), " ");
+ if (tokens.size() < 3) {
+ return false;
+ }
+
+ info->Timings.CpuWaitNsec = FromString<i64>(tokens[1]);
+ }
+
+ // Parse stat.
+ {
+ TIFStream file(statPath);
+ auto tokens = SplitString(file.ReadLine(), " ");
+ if (tokens.size() < 15) {
+ return false;
+ }
+
+ info->Timings.UserJiffies = FromString<i64>(tokens[13]);
+ info->Timings.SystemJiffies = FromString<i64>(tokens[14]);
+ }
+
+ info->ProfilingKey = info->ThreadName;
+
+ if (!TidToInfo_.contains(tid)) {
+ YT_LOG_TRACE("Thread %v named %v", tid, info->ThreadName);
+ }
+
+ // Group threads by thread pool, using YT thread naming convention.
+ if (auto index = info->ProfilingKey.rfind(':'); index != TString::npos) {
+ bool isDigit = std::all_of(info->ProfilingKey.cbegin() + index + 1, info->ProfilingKey.cend(), [] (char c) {
+ return std::isdigit(c);
+ });
+ if (isDigit) {
+ info->ProfilingKey = info->ProfilingKey.substr(0, index);
+ }
+ }
+
+ if (!info->IsYtThread) {
+ info->ProfilingKey += "@";
+ }
+ } catch (const TIoException&) {
+ // Ignore all IO exceptions.
+ return false;
+ }
+
+ YT_LOG_TRACE("Thread statistics (Tid: %v, ThreadName: %v, IsYtThread: %v, UserJiffies: %v, SystemJiffies: %v, CpuWaitNsec: %v)",
+ tid,
+ info->ThreadName,
+ info->IsYtThread,
+ info->Timings.UserJiffies,
+ info->Timings.SystemJiffies,
+ info->Timings.CpuWaitNsec);
+
+ return true;
+}
+
+TResourceTracker::TThreadMap TResourceTracker::ProcessThreads()
+{
+ TDirsList dirsList;
+ try {
+ dirsList.Fill(procPath);
+ } catch (const TSystemError&) {
+ // Ignore all exceptions.
+ return {};
+ }
+
+ TThreadMap tidToStats;
+
+ for (int index = 0; index < static_cast<int>(dirsList.Size()); ++index) {
+ auto tid = TString(dirsList.Next());
+ TThreadInfo info;
+ if (ProcessThread(tid, &info)) {
+ tidToStats[tid] = info;
+ } else {
+ YT_LOG_TRACE("Failed to prepare thread info for thread (Tid: %v)", tid);
+ }
+ }
+
+ return tidToStats;
+}
+
+void TResourceTracker::CollectSensorsAggregatedTimings(
+ ISensorWriter* writer,
+ const TResourceTracker::TThreadMap& oldTidToInfo,
+ const TResourceTracker::TThreadMap& newTidToInfo,
+ i64 timeDeltaUsec)
+{
+ double totalUserCpuTime = 0.0;
+ double totalSystemCpuTime = 0.0;
+ double totalCpuWaitTime = 0.0;
+
+ THashMap<TString, TTimings> profilingKeyToAggregatedTimings;
+ THashMap<TString, int> profilingKeyToCount;
+
+ // Consider only those threads which did not change their thread names.
+ // In each group of such threads with same thread name, export aggregated timings.
+
+ for (const auto& [tid, newInfo] : newTidToInfo) {
+ ++profilingKeyToCount[newInfo.ProfilingKey];
+
+ auto it = oldTidToInfo.find(tid);
+
+ if (it == oldTidToInfo.end()) {
+ continue;
+ }
+
+ const auto& oldInfo = it->second;
+
+ if (oldInfo.ProfilingKey != newInfo.ProfilingKey) {
+ continue;
+ }
+
+ profilingKeyToAggregatedTimings[newInfo.ProfilingKey] += newInfo.Timings - oldInfo.Timings;
+ }
+
+ double vCpuFactor = VCpuFactor_;
+
+ double maxUtilization = 0.0;
+ for (const auto& [profilingKey, aggregatedTimings] : profilingKeyToAggregatedTimings) {
+ // Multiplier 1e6 / timeDelta is for taking average over time (all values should be "per second").
+ // Multiplier 100 for CPU time is for measuring CPU load in percents. It is due to historical reasons.
+ double userCpuTime = std::max<double>(0.0, 100. * aggregatedTimings.UserJiffies / TicksPerSecond_ * (1e6 / timeDeltaUsec));
+ double systemCpuTime = std::max<double>(0.0, 100. * aggregatedTimings.SystemJiffies / TicksPerSecond_ * (1e6 / timeDeltaUsec));
+ double waitTime = std::max<double>(0.0, 100 * aggregatedTimings.CpuWaitNsec / 1e9 * (1e6 / timeDeltaUsec));
+
+ totalUserCpuTime += userCpuTime;
+ totalSystemCpuTime += systemCpuTime;
+ totalCpuWaitTime += waitTime;
+
+ auto threadCount = profilingKeyToCount[profilingKey];
+ double utilization = (userCpuTime + systemCpuTime) / (100 * threadCount);
+
+ double totalCpuTime = userCpuTime + systemCpuTime;
+
+ TWithTagGuard tagGuard(writer, "thread", profilingKey);
+ writer->AddGauge("/user_cpu", userCpuTime);
+ writer->AddGauge("/system_cpu", systemCpuTime);
+ writer->AddGauge("/total_cpu", totalCpuTime);
+ writer->AddGauge("/cpu_wait", waitTime);
+ writer->AddGauge("/thread_count", threadCount);
+ writer->AddGauge("/utilization", utilization);
+ if (vCpuFactor != 0.0) {
+ writer->AddGauge("/user_vcpu", userCpuTime * vCpuFactor);
+ writer->AddGauge("/system_vcpu", systemCpuTime * vCpuFactor);
+ writer->AddGauge("/total_vcpu", totalCpuTime * vCpuFactor);
+ }
+
+ maxUtilization = std::max(maxUtilization, utilization);
+
+ YT_LOG_TRACE("Thread CPU timings in percent/sec (ProfilingKey: %v, UserCpu: %v, SystemCpu: %v, CpuWait: %v)",
+ profilingKey,
+ userCpuTime,
+ systemCpuTime,
+ waitTime);
+ }
+
+ LastUserCpu_.store(totalUserCpuTime);
+ LastSystemCpu_.store(totalSystemCpuTime);
+ LastCpuWait_.store(totalCpuWaitTime);
+ MaxThreadPoolUtilization_ = maxUtilization;
+
+ YT_LOG_DEBUG("Total CPU timings in percent/sec (UserCpu: %v, SystemCpu: %v, CpuWait: %v)",
+ totalUserCpuTime,
+ totalSystemCpuTime,
+ totalCpuWaitTime);
+
+ int fileDescriptorCount = GetFileDescriptorCount();
+ writer->AddGauge("/open_fds", fileDescriptorCount);
+ YT_LOG_DEBUG("Assessed open file descriptors (Count: %v)", fileDescriptorCount);
+}
+
+double TResourceTracker::GetUserCpu()
+{
+ return LastUserCpu_.load();
+}
+
+double TResourceTracker::GetSystemCpu()
+{
+ return LastSystemCpu_.load();
+}
+
+double TResourceTracker::GetCpuWait()
+{
+ return LastCpuWait_.load();
+}
+
+i64 TResourceTracker::GetTotalMemoryLimit()
+{
+ return MemoryCgroupTracker_->GetTotalMemoryLimit();
+}
+
+TResourceTrackerPtr GetResourceTracker()
+{
+ return LeakyRefCountedSingleton<TResourceTracker>();
+}
+
+void EnableResourceTracker()
+{
+ GetResourceTracker();
+}
+
+void SetVCpuFactor(double vCpuFactor)
+{
+ GetResourceTracker()->SetVCpuFactor(vCpuFactor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/resource_tracker/resource_tracker.h b/yt/yt/library/profiling/resource_tracker/resource_tracker.h
new file mode 100644
index 0000000000..8287c74e47
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/resource_tracker.h
@@ -0,0 +1,132 @@
+#pragma once
+
+#include <vector>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/library/profiling/producer.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TCpuCgroupTracker)
+
+class TCpuCgroupTracker
+ : public NProfiling::ISensorProducer
+{
+public:
+ void CollectSensors(ISensorWriter* writer) override;
+
+private:
+ std::optional<TCgroupCpuStat> FirstCgroupStat_;
+ bool CgroupErrorLogged_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TMemoryCgroupTracker)
+
+class TMemoryCgroupTracker
+ : public NProfiling::ISensorProducer
+{
+public:
+ void CollectSensors(ISensorWriter* writer) override;
+
+ i64 GetTotalMemoryLimit();
+
+private:
+ bool CgroupErrorLogged_ = false;
+
+ std::atomic<i64> TotalMemoryLimit{0};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TResourceTracker)
+
+class TResourceTracker
+ : public NProfiling::ISensorProducer
+{
+public:
+ explicit TResourceTracker();
+
+ double GetUserCpu();
+ double GetSystemCpu();
+ double GetCpuWait();
+
+ i64 GetTotalMemoryLimit();
+
+ void CollectSensors(ISensorWriter* writer) override;
+
+ void SetVCpuFactor(double factor);
+
+private:
+ std::atomic<double> VCpuFactor_{0.0};
+
+ i64 TicksPerSecond_;
+ TInstant LastUpdateTime_;
+
+ // Value below are in percents.
+ std::atomic<double> LastUserCpu_{0.0};
+ std::atomic<double> LastSystemCpu_{0.0};
+ std::atomic<double> LastCpuWait_{0.0};
+
+ std::atomic<double> MaxThreadPoolUtilization_ = {0.0};
+
+ struct TTimings
+ {
+ i64 UserJiffies = 0;
+ i64 SystemJiffies = 0;
+ i64 CpuWaitNsec = 0;
+
+ TTimings operator-(const TTimings& other) const;
+ TTimings& operator+=(const TTimings& other);
+ };
+
+ struct TThreadInfo
+ {
+ TString ThreadName;
+ TTimings Timings;
+ bool IsYtThread = true;
+ //! This key is IsYtThread ? ThreadName : ThreadName + "@".
+ //! It allows to distinguish YT threads from non-YT threads that
+ //! inherited parent YT thread name.
+ TString ProfilingKey;
+ };
+
+ // thread id -> stats
+ using TThreadMap = THashMap<TString, TThreadInfo>;
+
+ TThreadMap TidToInfo_;
+
+ TCpuCgroupTrackerPtr CpuCgroupTracker_;
+ TMemoryCgroupTrackerPtr MemoryCgroupTracker_;
+
+ void EnqueueUsage();
+
+ void EnqueueThreadStats();
+
+ bool ProcessThread(TString tid, TThreadInfo* result);
+ TThreadMap ProcessThreads();
+
+ void CollectSensorsAggregatedTimings(
+ ISensorWriter* writer,
+ const TThreadMap& oldTidToInfo,
+ const TThreadMap& newTidToInfo,
+ i64 timeDeltaUsec);
+};
+
+TResourceTrackerPtr GetResourceTracker();
+
+void EnableResourceTracker();
+
+//! If this vCpuFactor is set, additional metrics will be reported:
+//! user, system, total cpu multiplied by given factor.
+//! E. g. system_vcpu = system_cpu * vcpu_factor.
+void SetVCpuFactor(double vCpuFactor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/resource_tracker/ya.make b/yt/yt/library/profiling/resource_tracker/ya.make
new file mode 100644
index 0000000000..39301641a0
--- /dev/null
+++ b/yt/yt/library/profiling/resource_tracker/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ resource_tracker.cpp
+)
+
+PEERDIR(
+ yt/yt/library/profiling
+
+ # TODO(prime@:) remove this, once dependency cycle with yt/core is resolved
+ yt/yt_proto/yt/core
+)
+
+END()
diff --git a/yt/yt/library/profiling/sensor.cpp b/yt/yt/library/profiling/sensor.cpp
new file mode 100644
index 0000000000..006207771a
--- /dev/null
+++ b/yt/yt/library/profiling/sensor.cpp
@@ -0,0 +1,659 @@
+#include "sensor.h"
+#include "impl.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/string/format.h>
+
+#include <util/system/compiler.h>
+
+#include <atomic>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCounter::Increment(i64 delta) const
+{
+ if (!Counter_) {
+ return;
+ }
+
+ Counter_->Increment(delta);
+}
+
+TCounter::operator bool() const
+{
+ return Counter_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTimeCounter::Add(TDuration delta) const
+{
+ if (!Counter_) {
+ return;
+ }
+
+ Counter_->Add(delta);
+}
+
+TTimeCounter::operator bool() const
+{
+ return Counter_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TGauge::Update(double value) const
+{
+ if (!Gauge_) {
+ return;
+ }
+
+ Gauge_->Update(value);
+}
+
+TGauge::operator bool() const
+{
+ return Gauge_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTimeGauge::Update(TDuration value) const
+{
+ if (!Gauge_) {
+ return;
+ }
+
+ Gauge_->Update(value);
+}
+
+TTimeGauge::operator bool() const
+{
+ return Gauge_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSummary::Record(double value) const
+{
+ if (!Summary_) {
+ return;
+ }
+
+ Summary_->Record(value);
+}
+
+TSummary::operator bool() const
+{
+ return Summary_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TEventTimer::Record(TDuration value) const
+{
+ if (!Timer_) {
+ return;
+ }
+
+ Timer_->Record(value);
+}
+
+TEventTimer::operator bool() const
+{
+ return Timer_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEventTimerGuard::TEventTimerGuard(TEventTimer timer)
+ : Timer_(std::move(timer))
+ , StartTime_(GetCpuInstant())
+{ }
+
+TEventTimerGuard::TEventTimerGuard(TTimeGauge timeGauge)
+ : TimeGauge_(std::move(timeGauge))
+ , StartTime_(GetCpuInstant())
+{ }
+
+TEventTimerGuard::~TEventTimerGuard()
+{
+ if (!Timer_ && !TimeGauge_) {
+ return;
+ }
+
+ auto duration = GetElapsedTime();
+ if (Timer_) {
+ Timer_.Record(duration);
+ }
+ if (TimeGauge_) {
+ TimeGauge_.Update(duration);
+ }
+}
+
+TDuration TEventTimerGuard::GetElapsedTime() const
+{
+ return CpuDurationToDuration(GetCpuInstant() - StartTime_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TGaugeHistogram::Add(double value, int count) noexcept
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->Add(value, count);
+}
+
+void TGaugeHistogram::Remove(double value, int count) noexcept
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->Remove(value, count);
+}
+
+void TGaugeHistogram::Reset() noexcept
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->Reset();
+}
+
+THistogramSnapshot TGaugeHistogram::GetSnapshot() const
+{
+ if (!Histogram_) {
+ return {};
+ }
+
+ return Histogram_->GetSnapshot(false);
+}
+
+void TGaugeHistogram::LoadSnapshot(THistogramSnapshot snapshot)
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->LoadSnapshot(snapshot);
+}
+
+TGaugeHistogram::operator bool() const
+{
+ return Histogram_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRateHistogram::Add(double value, int count) noexcept
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->Add(value, count);
+}
+
+void TRateHistogram::Remove(double value, int count) noexcept
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->Remove(value, count);
+}
+
+void TRateHistogram::Reset() noexcept
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->Reset();
+}
+
+THistogramSnapshot TRateHistogram::GetSnapshot() const
+{
+ if (!Histogram_) {
+ return {};
+ }
+
+ return Histogram_->GetSnapshot(false);
+}
+
+void TRateHistogram::LoadSnapshot(THistogramSnapshot snapshot)
+{
+ if (!Histogram_) {
+ return;
+ }
+
+ Histogram_->LoadSnapshot(snapshot);
+}
+
+TRateHistogram::operator bool() const
+{
+ return Histogram_.operator bool();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TSensorOptions& options)
+{
+ return Format(
+ "{sparse=%v;global=%v;hot=%v;histogram_min=%v;histogram_max=%v;time_histogram_bounds=%v;histogram_bounds=%v}",
+ options.Sparse,
+ options.Global,
+ options.Hot,
+ options.HistogramMin,
+ options.HistogramMax,
+ options.TimeHistogramBounds,
+ options.HistogramBounds
+ );
+}
+
+bool TSensorOptions::IsCompatibleWith(const TSensorOptions& other) const
+{
+ return Sparse == other.Sparse &&
+ Global == other.Global &&
+ DisableSensorsRename == other.DisableSensorsRename &&
+ DisableDefault == other.DisableDefault &&
+ DisableProjections == other.DisableProjections;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProfiler::TProfiler(
+ const IRegistryImplPtr& impl,
+ const TString& prefix,
+ const TString& _namespace)
+ : Enabled_(true)
+ , Prefix_(prefix)
+ , Namespace_(_namespace)
+ , Impl_(impl)
+{ }
+
+TProfiler::TProfiler(
+ const TString& prefix,
+ const TString& _namespace,
+ const TTagSet& tags,
+ const IRegistryImplPtr& impl,
+ TSensorOptions options)
+ : Enabled_(true)
+ , Prefix_(prefix)
+ , Namespace_(_namespace)
+ , Tags_(tags)
+ , Options_(options)
+ , Impl_(impl ? impl : GetGlobalRegistry())
+{ }
+
+TProfiler TProfiler::WithPrefix(const TString& prefix) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ return TProfiler(Prefix_ + prefix, Namespace_, Tags_, Impl_, Options_);
+}
+
+TProfiler TProfiler::WithTag(const TString& name, const TString& value, int parent) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto allTags = Tags_;
+ allTags.AddTag(std::pair(name, value), parent);
+ return TProfiler(Prefix_, Namespace_, allTags, Impl_, Options_);
+}
+
+void TProfiler::RenameDynamicTag(const TDynamicTagPtr& tag, const TString& name, const TString& value) const
+{
+ if (!Impl_) {
+ return;
+ }
+
+ Impl_->RenameDynamicTag(tag, name, value);
+}
+
+TProfiler TProfiler::WithRequiredTag(const TString& name, const TString& value, int parent) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto allTags = Tags_;
+ allTags.AddRequiredTag(std::pair(name, value), parent);
+ return TProfiler(Prefix_, Namespace_, allTags, Impl_, Options_);
+}
+
+TProfiler TProfiler::WithExcludedTag(const TString& name, const TString& value, int parent) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto allTags = Tags_;
+ allTags.AddExcludedTag(std::pair(name, value), parent);
+ return TProfiler(Prefix_, Namespace_, allTags, Impl_, Options_);
+}
+
+TProfiler TProfiler::WithAlternativeTag(const TString& name, const TString& value, int alternativeTo, int parent) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto allTags = Tags_;
+
+ allTags.AddAlternativeTag(std::pair(name, value), alternativeTo, parent);
+ return TProfiler(Prefix_, Namespace_, allTags, Impl_, Options_);
+}
+
+TProfiler TProfiler::WithExtensionTag(const TString& name, const TString& value, int extensionOf) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto allTags = Tags_;
+
+ allTags.AddExtensionTag(std::pair(name, value), extensionOf);
+ return TProfiler(Prefix_, Namespace_, allTags, Impl_, Options_);
+}
+
+TProfiler TProfiler::WithTags(const TTagSet& tags) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto allTags = Tags_;
+ allTags.Append(tags);
+ return TProfiler(Prefix_, Namespace_, allTags, Impl_, Options_);
+}
+
+TProfiler TProfiler::WithSparse() const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto opts = Options_;
+ opts.Sparse = true;
+ return TProfiler(Prefix_, Namespace_, Tags_, Impl_, opts);
+}
+
+TProfiler TProfiler::WithDense() const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto opts = Options_;
+ opts.Sparse = false;
+ return TProfiler(Prefix_, Namespace_, Tags_, Impl_, opts);
+}
+
+TProfiler TProfiler::WithGlobal() const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto opts = Options_;
+ opts.Global = true;
+ return TProfiler(Prefix_, Namespace_, Tags_, Impl_, opts);
+}
+
+TProfiler TProfiler::WithDefaultDisabled() const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto opts = Options_;
+ opts.DisableDefault = true;
+ return TProfiler(Prefix_, Namespace_, Tags_, Impl_, opts);
+}
+
+TProfiler TProfiler::WithProjectionsDisabled() const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto allTags = Tags_;
+ allTags.SetEnabled(false);
+
+ auto opts = Options_;
+ opts.DisableProjections = true;
+
+ return TProfiler(Prefix_, Namespace_, allTags, Impl_, opts);
+}
+
+TProfiler TProfiler::WithRenameDisabled() const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto opts = Options_;
+ opts.DisableSensorsRename = true;
+ return TProfiler(Prefix_, Namespace_, Tags_, Impl_, opts);
+}
+
+TProfiler TProfiler::WithProducerRemoveSupport() const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto opts = Options_;
+ opts.ProducerRemoveSupport = true;
+ return TProfiler(Prefix_, Namespace_, Tags_, Impl_, opts);
+}
+
+TProfiler TProfiler::WithHot(bool value) const
+{
+ if (!Enabled_) {
+ return {};
+ }
+
+ auto opts = Options_;
+ opts.Hot = value;
+ return TProfiler(Prefix_, Namespace_, Tags_, Impl_, opts);
+}
+
+TCounter TProfiler::Counter(const TString& name) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TCounter counter;
+ counter.Counter_ = Impl_->RegisterCounter(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return counter;
+}
+
+TTimeCounter TProfiler::TimeCounter(const TString& name) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TTimeCounter counter;
+ counter.Counter_ = Impl_->RegisterTimeCounter(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return counter;
+}
+
+TGauge TProfiler::Gauge(const TString& name) const
+{
+ if (!Impl_) {
+ return TGauge();
+ }
+
+ TGauge gauge;
+ gauge.Gauge_ = Impl_->RegisterGauge(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return gauge;
+}
+
+TTimeGauge TProfiler::TimeGauge(const TString& name) const
+{
+ if (!Impl_) {
+ return TTimeGauge();
+ }
+
+ TTimeGauge gauge;
+ gauge.Gauge_ = Impl_->RegisterTimeGauge(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return gauge;
+}
+
+TSummary TProfiler::Summary(const TString& name) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TSummary summary;
+ summary.Summary_ = Impl_->RegisterSummary(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return summary;
+}
+
+TGauge TProfiler::GaugeSummary(const TString& name) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TGauge gauge;
+ gauge.Gauge_ = Impl_->RegisterGaugeSummary(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return gauge;
+}
+
+TTimeGauge TProfiler::TimeGaugeSummary(const TString& name) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TTimeGauge gauge;
+ gauge.Gauge_ = Impl_->RegisterTimeGaugeSummary(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return gauge;
+}
+
+TEventTimer TProfiler::Timer(const TString& name) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TEventTimer timer;
+ timer.Timer_ = Impl_->RegisterTimerSummary(Namespace_ + Prefix_ + name, Tags_, Options_);
+ return timer;
+}
+
+TEventTimer TProfiler::TimeHistogram(const TString& name, TDuration min, TDuration max) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ auto options = Options_;
+ options.HistogramMin = min;
+ options.HistogramMax = max;
+
+ TEventTimer timer;
+ timer.Timer_ = Impl_->RegisterTimeHistogram(Namespace_ + Prefix_ + name, Tags_, options);
+ return timer;
+}
+
+TEventTimer TProfiler::TimeHistogram(const TString& name, std::vector<TDuration> bounds) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TEventTimer timer;
+ auto options = Options_;
+ options.TimeHistogramBounds = std::move(bounds);
+ timer.Timer_ = Impl_->RegisterTimeHistogram(Namespace_ + Prefix_ + name, Tags_, options);
+ return timer;
+}
+
+TGaugeHistogram TProfiler::GaugeHistogram(const TString& name, std::vector<double> buckets) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TGaugeHistogram histogram;
+ auto options = Options_;
+ options.HistogramBounds = std::move(buckets);
+ histogram.Histogram_ = Impl_->RegisterGaugeHistogram(Namespace_ + Prefix_ + name, Tags_, options);
+ return histogram;
+}
+
+TRateHistogram TProfiler::RateHistogram(const TString& name, std::vector<double> buckets) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ TRateHistogram histogram;
+ auto options = Options_;
+ options.HistogramBounds = std::move(buckets);
+ histogram.Histogram_ = Impl_->RegisterRateHistogram(Namespace_ + Prefix_ + name, Tags_, options);
+ return histogram;
+}
+
+void TProfiler::AddFuncCounter(
+ const TString& name,
+ const TRefCountedPtr& owner,
+ std::function<i64()> reader) const
+{
+ if (!Impl_) {
+ return;
+ }
+
+ Impl_->RegisterFuncCounter(Namespace_ + Prefix_ + name, Tags_, Options_, owner, reader);
+}
+
+void TProfiler::AddFuncGauge(
+ const TString& name,
+ const TRefCountedPtr& owner,
+ std::function<double()> reader) const
+{
+ if (!Impl_) {
+ return;
+ }
+
+ Impl_->RegisterFuncGauge(Namespace_ + Prefix_ + name, Tags_, Options_, owner, reader);
+}
+
+void TProfiler::AddProducer(
+ const TString& prefix,
+ const ISensorProducerPtr& producer) const
+{
+ if (!Impl_) {
+ return;
+ }
+
+ Impl_->RegisterProducer(Namespace_ + Prefix_ + prefix, Tags_, Options_, producer);
+}
+
+const IRegistryImplPtr& TProfiler::GetRegistry() const
+{
+ return Impl_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/sensor.h b/yt/yt/library/profiling/sensor.h
new file mode 100644
index 0000000000..e20031d710
--- /dev/null
+++ b/yt/yt/library/profiling/sensor.h
@@ -0,0 +1,408 @@
+#pragma once
+
+#include "public.h"
+#include "tag.h"
+#include "histogram_snapshot.h"
+
+#include <library/cpp/yt/memory/intrusive_ptr.h>
+#include <library/cpp/yt/memory/weak_ptr.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+#include <vector>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCounter
+{
+public:
+ //! Inc increments counter.
+ /*!
+ * @delta MUST be >= 0.
+ */
+ void Increment(i64 delta = 1) const;
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+ friend struct TTesting;
+
+ ICounterImplPtr Counter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimeCounter
+{
+public:
+ void Add(TDuration delta) const;
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+ friend struct TTesting;
+
+ ITimeCounterImplPtr Counter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGauge
+{
+public:
+ void Update(double value) const;
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+ friend struct TTesting;
+
+ IGaugeImplPtr Gauge_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimeGauge
+{
+public:
+ void Update(TDuration value) const;
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+ friend struct TTesting;
+
+ ITimeGaugeImplPtr Gauge_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSummary
+{
+public:
+ void Record(double value) const;
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+
+ ISummaryImplPtr Summary_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEventTimer
+{
+public:
+ void Record(TDuration value) const;
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+
+ ITimerImplPtr Timer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEventTimerGuard
+{
+public:
+ explicit TEventTimerGuard(TEventTimer timer);
+ explicit TEventTimerGuard(TTimeGauge gauge);
+ TEventTimerGuard(TEventTimerGuard&& other) = default;
+ ~TEventTimerGuard();
+
+ TDuration GetElapsedTime() const;
+
+private:
+ TEventTimer Timer_;
+ TTimeGauge TimeGauge_;
+ TCpuInstant StartTime_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGaugeHistogram
+{
+public:
+ void Add(double value, int count = 1) noexcept;
+ void Remove(double value, int count = 1) noexcept;
+ void Reset() noexcept;
+
+ THistogramSnapshot GetSnapshot() const;
+ void LoadSnapshot(THistogramSnapshot snapshot);
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+
+ IHistogramImplPtr Histogram_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRateHistogram
+{
+public:
+ void Add(double value, int count = 1) noexcept;
+ void Remove(double value, int count = 1) noexcept;
+ void Reset() noexcept;
+
+ THistogramSnapshot GetSnapshot() const;
+ void LoadSnapshot(THistogramSnapshot snapshot);
+
+ explicit operator bool() const;
+
+private:
+ friend class TProfiler;
+
+ IHistogramImplPtr Histogram_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSensorOptions
+{
+ bool Global = false;
+ bool Sparse = false;
+ bool Hot = false;
+ bool DisableSensorsRename = false;
+ bool DisableDefault = false;
+ bool DisableProjections = false;
+ bool ProducerRemoveSupport = false;
+
+ TDuration HistogramMin;
+ TDuration HistogramMax;
+
+ std::vector<TDuration> TimeHistogramBounds;
+
+ std::vector<double> HistogramBounds;
+
+ bool IsCompatibleWith(const TSensorOptions& other) const;
+};
+
+TString ToString(const TSensorOptions& options);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! TProfiler stores common settings of profiling counters.
+class TProfiler
+{
+public:
+ //! Default constructor creates null registry. Every method of null registry is no-op.
+ /*!
+ * Default constructor is useful for implementing optional profiling. E.g:
+ *
+ * TCache CreateCache(const TProfiler& profiler = {});
+ *
+ * void Example()
+ * {
+ * auto cache = CreateCache(); // Create cache without profiling
+ * auto profiledCache = CreateCache(TProfiler{"/my_cache"}); // Enable profiling
+ * }
+ */
+ TProfiler() = default;
+
+ static constexpr auto DefaultNamespace = "yt";
+
+ TProfiler(
+ const IRegistryImplPtr& impl,
+ const TString& prefix,
+ const TString& _namespace = DefaultNamespace);
+
+ explicit TProfiler(
+ const TString& prefix,
+ const TString& _namespace = DefaultNamespace,
+ const TTagSet& tags = {},
+ const IRegistryImplPtr& impl = nullptr,
+ TSensorOptions options = {});
+
+ TProfiler WithPrefix(const TString& prefix) const;
+
+ //! Tag settings control local aggregates.
+ /*!
+ * See README.md for more details.
+ * #parent is negative number representing parent tag index.
+ * #alternativeTo is negative number representing alternative tag index.
+ */
+ TProfiler WithTag(const TString& name, const TString& value, int parent = NoParent) const;
+ TProfiler WithRequiredTag(const TString& name, const TString& value, int parent = NoParent) const;
+ TProfiler WithExcludedTag(const TString& name, const TString& value, int parent = NoParent) const;
+ TProfiler WithAlternativeTag(const TString& name, const TString& value, int alternativeTo, int parent = NoParent) const;
+ TProfiler WithExtensionTag(const TString& name, const TString& value, int parent = NoParent) const;
+ TProfiler WithTags(const TTagSet& tags) const;
+
+ //! Rename tag in all previously registered sensors.
+ /*!
+ * NOTE: this is O(n) operation.
+ */
+ void RenameDynamicTag(const TDynamicTagPtr& tag, const TString& name, const TString& value) const;
+
+ //! WithSparse sets sparse flags on all sensors created using returned registry.
+ /*!
+ * Sparse sensors with zero value are omitted from profiling results.
+ */
+ TProfiler WithSparse() const;
+
+ //! WithDense clears sparse flags on all sensors created using returned registry.
+ TProfiler WithDense() const;
+
+ //! WithGlobal marks all sensors as global.
+ /*!
+ * Global sensors are exported without host= tag and instance tags.
+ */
+ TProfiler WithGlobal() const;
+
+ //! WithDefaultDisabled disables export of default values.
+ /*!
+ * By default, gauges report zero value after creation. With this setting enabled,
+ * gauges are not exported before first call to Update().
+ */
+ TProfiler WithDefaultDisabled() const;
+
+ //! WithProjectionsDisabled disables local aggregation.
+ TProfiler WithProjectionsDisabled() const;
+
+ //! WithRenameDisabled disables sensors name normalization.
+ TProfiler WithRenameDisabled() const;
+
+ //! WithProducerRemoveSupport removes sensors that were absent on producer iteration.
+ /*!
+ * By default, if sensor is absent on producer iteration, profiler keeps repeating
+ * previous sensor value.
+ */
+ TProfiler WithProducerRemoveSupport() const;
+
+ //! WithHot sets hot flag on all sensors created using returned registry.
+ /*!
+ * Hot sensors are implemented using per-cpu sharding, that increases
+ * performance under contention, but also increases memory consumption.
+ *
+ * Default implementation:
+ * 24 bytes - Counter, TimeCounter and Gauge
+ * 64 bytes - Timer and Summary
+ *
+ * Per-CPU implementation:
+ * 4160 bytes - Counter, TimeCounter, Gauge, Timer, Summary
+ */
+ TProfiler WithHot(bool value = true) const;
+
+ //! Counter is used to measure rate of events.
+ TCounter Counter(const TString& name) const;
+
+ //! Counter is used to measure CPU time consumption.
+ TTimeCounter TimeCounter(const TString& name) const;
+
+ //! Gauge is used to measure instant value.
+ TGauge Gauge(const TString& name) const;
+
+ //! TimeGauge is used to measure instant duration.
+ TTimeGauge TimeGauge(const TString& name) const;
+
+ //! Summary is used to measure distribution of values.
+ TSummary Summary(const TString& name) const;
+
+ //! GaugeSummary is used to aggregate multiple values locally.
+ /*!
+ * Each TGauge tracks single value. Values are aggregated using Summary rules.
+ */
+ TGauge GaugeSummary(const TString& name) const;
+
+ //! TimeGaugeSummary is used to aggregate multiple values locally.
+ /*!
+ * Each TGauge tracks single value. Values are aggregated using Summary rules.
+ */
+ TTimeGauge TimeGaugeSummary(const TString& name) const;
+
+ //! Timer is used to measure distribution of event durations.
+ /*!
+ * Currently, max value during 5 second interval is exported to solomon.
+ * Use it, when you need a cheap way to monitor lag spikes.
+ */
+ TEventTimer Timer(const TString& name) const;
+
+ //! TimeHistogram is used to measure distribution of event durations.
+ /*!
+ * Bins are distributed _almost_ exponentially with step of 2; the only difference is that 64
+ * is followed by 125, 64'000 is followed by 125'000 and so on for the sake of better human-readability
+ * of upper limit.
+ *
+ * The first several bin marks are:
+ * 1, 2, 4, 8, 16, 32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16'000, 32'000, 64'000, 125'000, ...
+ *
+ * In terms of time this can be read as:
+ * 1us, 2us, 4us, 8us, ..., 500us, 1ms, 2ms, ..., 500ms, 1s, ...
+ */
+ TEventTimer TimeHistogram(const TString& name, TDuration min, TDuration max) const;
+
+ //! TimeHistogram is used to measure distribution of event durations.
+ /*!
+ * Allows to use custom bounds, bounds should be sorted (maximum 51 elements are allowed).
+ */
+ TEventTimer TimeHistogram(const TString& name, std::vector<TDuration> bounds) const;
+
+ //! GaugeHistogram is used to measure distribution of set of samples.
+ TGaugeHistogram GaugeHistogram(const TString& name, std::vector<double> buckets) const;
+
+ //! RateHistogram is used to measure distribution of set of samples.
+ /*!
+ * Bucket values at the next point will be calculated as a derivative.
+ */
+ TRateHistogram RateHistogram(const TString& name, std::vector<double> buckets) const;
+
+ void AddFuncCounter(
+ const TString& name,
+ const TRefCountedPtr& owner,
+ std::function<i64()> reader) const;
+
+ void AddFuncGauge(
+ const TString& name,
+ const TRefCountedPtr& owner,
+ std::function<double()> reader) const;
+
+ void AddProducer(
+ const TString& prefix,
+ const ISensorProducerPtr& producer) const;
+
+ const IRegistryImplPtr& GetRegistry() const;
+
+private:
+ friend struct TTesting;
+
+ bool Enabled_ = false;
+ TString Prefix_;
+ TString Namespace_;
+ TTagSet Tags_;
+ TSensorOptions Options_;
+ IRegistryImplPtr Impl_;
+};
+
+using TRegistry = TProfiler;
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define YT_PROFILE_GENNAME0(line) PROFILE_TIMING__ ## line
+#define YT_PROFILE_GENNAME(line) YT_PROFILE_GENNAME0(line)
+
+//! Measures execution time of the statement that immediately follows this macro.
+#define YT_PROFILE_TIMING(name) \
+ static auto YT_PROFILE_GENNAME(__LINE__) = ::NYT::NProfiling::TProfiler{name}.WithHot().Timer(""); \
+ if (auto PROFILE_TIMING__Guard = ::NYT::NProfiling::TEventTimerGuard(YT_PROFILE_GENNAME(__LINE__)); false) \
+ { YT_ABORT(); } \
+ else
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/cube.cpp b/yt/yt/library/profiling/solomon/cube.cpp
new file mode 100644
index 0000000000..213fc560aa
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/cube.cpp
@@ -0,0 +1,724 @@
+#include "cube.h"
+#include "remote.h"
+
+#include <yt/yt/library/profiling/summary.h>
+#include <yt/yt/library/profiling/tag.h>
+#include <yt/yt/library/profiling/histogram_snapshot.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <type_traits>
+
+namespace NYT::NProfiling {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+bool TCube<T>::TProjection::IsZero(int index) const
+{
+ return IsZeroValue(Values[index]);
+}
+
+template <class T>
+bool TCube<T>::TProjection::IsLingering(i64 iteration) const
+{
+ return LastNonZeroIteration >= iteration;
+}
+
+template <class T>
+TCube<T>::TCube(int windowSize, i64 nextIteration)
+ : WindowSize_(windowSize)
+ , NextIteration_(nextIteration)
+ , BaseIteration_(nextIteration - (nextIteration % windowSize))
+{ }
+
+template <class T>
+void TCube<T>::StartIteration()
+{
+ Index_ = GetIndex(NextIteration_);
+ NextIteration_++;
+ BaseIteration_ = NextIteration_ - (NextIteration_ % WindowSize_);
+
+ for (auto& [tagIds, projection] : Projections_) {
+ projection.Rollup += projection.Values[Index_];
+ projection.Values[Index_] = {};
+ projection.HasValue[Index_] = false;
+ }
+}
+
+template <class T>
+void TCube<T>::FinishIteration()
+{ }
+
+template <class T>
+void TCube<T>::Add(TTagIdList tagIds)
+{
+ std::sort(tagIds.begin(), tagIds.end());
+ if (auto it = Projections_.find(tagIds); it != Projections_.end()) {
+ it->second.UsageCount++;
+ } else {
+ TProjection projection;
+ projection.UsageCount = 1;
+ projection.Values.resize(WindowSize_);
+ projection.HasValue.resize(WindowSize_);
+ Projections_[tagIds] = std::move(projection);
+ }
+}
+
+template <class T>
+void TCube<T>::AddAll(const TTagIdList& tagIds, const TProjectionSet& projections)
+{
+ projections.Range(tagIds, [this] (auto tagIds) mutable {
+ Add(std::move(tagIds));
+ });
+}
+
+template <class T>
+void TCube<T>::Remove(TTagIdList tagIds)
+{
+ std::sort(tagIds.begin(), tagIds.end());
+ auto it = Projections_.find(tagIds);
+ if (it == Projections_.end()) {
+ THROW_ERROR_EXCEPTION("Can't remove tags from cube")
+ << TErrorAttribute("tag_ids", tagIds);
+ }
+
+ it->second.UsageCount--;
+ if (it->second.UsageCount == 0) {
+ Projections_.erase(it);
+ }
+}
+
+template <class T>
+void TCube<T>::RemoveAll(const TTagIdList& tagIds, const TProjectionSet& projections)
+{
+ projections.Range(tagIds, [this] (auto tagIds) mutable {
+ Remove(std::move(tagIds));
+ });
+}
+
+template <class T>
+void TCube<T>::Update(TTagIdList tagIds, T value)
+{
+ std::sort(tagIds.begin(), tagIds.end());
+ auto it = Projections_.find(tagIds);
+ if (it == Projections_.end()) {
+ THROW_ERROR_EXCEPTION("Can't update tags in cube")
+ << TErrorAttribute("tag_ids", tagIds);
+ }
+
+ if constexpr (std::is_same_v<T, double>) {
+ // Special value for gauges with DisableDefault option enabled.
+ if (std::isnan(value)) {
+ return;
+ }
+ }
+
+ it->second.Values[Index_] += value;
+ it->second.HasValue[Index_] = true;
+ if (!it->second.IsZero(Index_)) {
+ it->second.LastNonZeroIteration = NextIteration_ - 1;
+ }
+}
+
+template <class T>
+const THashMap<TTagIdList, typename TCube<T>::TProjection>& TCube<T>::GetProjections() const
+{
+ return Projections_;
+}
+
+template <class T>
+int TCube<T>::GetSize() const
+{
+ return Projections_.size();
+}
+
+template <class T>
+int TCube<T>::GetIndex(i64 iteration) const
+{
+ return iteration % WindowSize_;
+}
+
+template <class T>
+i64 TCube<T>::GetIteration(int index) const
+{
+ auto iteration = BaseIteration_ + index;
+ if (iteration >= NextIteration_) {
+ iteration -= WindowSize_;
+ }
+ return iteration;
+}
+
+template <class T>
+T TCube<T>::Rollup(const TProjection& window, int index) const
+{
+ auto sum = window.Rollup;
+
+ for (auto i = Index_ + 1; true; i++) {
+ if (i == WindowSize_) {
+ i = 0;
+ }
+
+ sum += window.Values[i];
+ if (i == index) {
+ break;
+ }
+ }
+
+ return sum;
+}
+
+template <class T>
+int TCube<T>::ReadSensors(
+ const TString& name,
+ const TReadOptions& options,
+ TTagWriter* tagWriter,
+ ::NMonitoring::IMetricConsumer* consumer) const
+{
+ int sensorsEmitted = 0;
+
+ auto prepareNameLabel = [&] (std::optional<TStringBuf> suffix) {
+ TString sensorName;
+ sensorName.reserve(name.size() + (suffix ? suffix->size() : 0));
+ if (options.DisableSensorsRename) {
+ sensorName += name;
+ } else if (options.StripSensorsNamePrefix) {
+ auto delimiterPos = name.find_last_of('/');
+ if (TString::npos == delimiterPos) {
+ sensorName.assign(name);
+ } else {
+ sensorName.assign(name, delimiterPos + 1);
+ }
+ } else {
+ if (name[0] != '/') {
+ sensorName.push_back(name[0]);
+ }
+ for (size_t i = 1; i < name.size(); ++i) {
+ if (name[i] == '/') {
+ sensorName.push_back('.');
+ } else {
+ sensorName.push_back(name[i]);
+ }
+ }
+ if (sensorName.back() == '.') {
+ sensorName.pop_back();
+ }
+ }
+
+ if (suffix) {
+ sensorName += *suffix;
+ }
+
+ return consumer->PrepareLabel("sensor", sensorName);
+ };
+
+ auto nameLabel = prepareNameLabel({});
+ auto maxNameLabel = prepareNameLabel(".max");
+ auto avgNameLabel = prepareNameLabel(".avg");
+ auto rateNameLabel = prepareNameLabel(".rate");
+ auto globalHostLabel = consumer->PrepareLabel("host", "");
+ auto hostLabel = consumer->PrepareLabel("host", options.Host.value_or(""));
+ auto ytAggrLabel = consumer->PrepareLabel("yt_aggr", "1");
+
+ auto writeLabels = [&] (const auto& tagIds, std::pair<ui32, ui32> nameLabel, bool allowAggregate) {
+ consumer->OnLabelsBegin();
+
+ consumer->OnLabel(nameLabel.first, nameLabel.second);
+
+ if (options.Global) {
+ consumer->OnLabel(globalHostLabel.first, globalHostLabel.second);
+ } else if (options.Host) {
+ consumer->OnLabel(hostLabel.first, hostLabel.second);
+ }
+
+ TCompactVector<bool, 8> replacedInstanceTags(options.InstanceTags.size());
+
+ if (allowAggregate && options.MarkAggregates && !options.Global) {
+ consumer->OnLabel(ytAggrLabel.first, ytAggrLabel.second);
+ }
+
+ for (auto tagId : tagIds) {
+ const auto& tag = tagWriter->Decode(tagId);
+
+ for (size_t i = 0; i < options.InstanceTags.size(); i++) {
+ if (options.InstanceTags[i].first == tag.first) {
+ replacedInstanceTags[i] = true;
+ }
+ }
+
+ tagWriter->WriteLabel(tagId);
+ }
+
+ if (!options.Global) {
+ for (size_t i = 0; i < options.InstanceTags.size(); i++) {
+ if (replacedInstanceTags[i]) {
+ continue;
+ }
+
+ const auto& tag = options.InstanceTags[i];
+ consumer->OnLabel(tag.first, tag.second);
+ }
+ }
+
+ consumer->OnLabelsEnd();
+ };
+
+ auto skipByHack = [&] (const auto& window) {
+ if (!options.Sparse) {
+ return false;
+ }
+
+ for (const auto& readBatch : options.Times) {
+ for (auto index : readBatch.first) {
+ if (window.IsLingering(GetIteration(index) - options.LingerWindowSize)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ };
+
+ auto skipSparse = [&] (auto window, const std::vector<int>& indices) {
+ if (!options.Sparse) {
+ return false;
+ }
+
+ for (auto index : indices) {
+ if (window.IsLingering(GetIteration(index) - options.LingerWindowSize)) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+
+ for (const auto& [tagIds, window] : Projections_) {
+ if (options.EnableSolomonAggregationWorkaround && skipByHack(window)) {
+ continue;
+ }
+
+ int sensorCount = 0;
+
+ bool empty = true;
+ for (const auto& [indices, time] : options.Times) {
+ if (!options.EnableSolomonAggregationWorkaround && skipSparse(window, indices)) {
+ continue;
+ }
+
+ empty = false;
+ }
+ if (empty) {
+ continue;
+ }
+
+ auto rangeValues = [&, window=&window] (auto cb) {
+ for (const auto& [indices, time] : options.Times) {
+ if (!options.EnableSolomonAggregationWorkaround && skipSparse(*window, indices)) {
+ continue;
+ }
+
+ T value{};
+ for (auto index : indices) {
+ if (index < 0 || static_cast<size_t>(index) >= window->Values.size()) {
+ THROW_ERROR_EXCEPTION("Read index is invalid")
+ << TErrorAttribute("index", index)
+ << TErrorAttribute("window_size", window->Values.size());
+ }
+
+ value += window->Values[index];
+ }
+
+ cb(value, time, indices);
+ }
+ };
+
+ auto writeSummary = [&, tagIds=tagIds] (auto makeSummary) {
+ if (options.ExportSummary) {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::DSUMMARY);
+ writeLabels(tagIds, nameLabel, true);
+
+ rangeValues([&] (auto value, auto time, const auto& /* indices */) {
+ sensorCount += 5;
+ consumer->OnSummaryDouble(time, makeSummary(value));
+ });
+
+ consumer->OnMetricEnd();
+ }
+
+ if (options.ExportSummaryAsMax) {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::GAUGE);
+ writeLabels(tagIds, maxNameLabel, false);
+
+ rangeValues([&] (auto value, auto time, const auto& /* indices */) {
+ sensorCount += 1;
+ consumer->OnDouble(time, makeSummary(value)->GetMax());
+ });
+
+ consumer->OnMetricEnd();
+ }
+
+ if (options.ExportSummaryAsAvg) {
+ bool empty = true;
+
+ rangeValues([&] (auto value, auto time, const auto& /* indices */) {
+ auto snapshot = makeSummary(value);
+ if (snapshot->GetCount() == 0) {
+ return;
+ }
+
+ if (empty) {
+ empty = false;
+ consumer->OnMetricBegin(NMonitoring::EMetricType::GAUGE);
+ writeLabels(tagIds, avgNameLabel, false);
+ }
+
+ sensorCount += 1;
+
+ consumer->OnDouble(time, snapshot->GetSum() / snapshot->GetCount());
+ });
+
+ if (!empty) {
+ consumer->OnMetricEnd();
+ }
+ }
+ };
+
+ if constexpr (std::is_same_v<T, i64> || std::is_same_v<T, TDuration>) {
+ if (options.ConvertCountersToRateGauge) {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::GAUGE);
+ } else {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::RATE);
+ }
+
+ writeLabels(tagIds, (options.ConvertCountersToRateGauge && options.RenameConvertedCounters) ? rateNameLabel : nameLabel, true);
+
+ rangeValues([&, window=&window] (auto value, auto time, const auto& indices) {
+ sensorCount += 1;
+ if (options.ConvertCountersToRateGauge) {
+ if (options.RateDenominator < 0.1) {
+ THROW_ERROR_EXCEPTION("Invalid rate denominator");
+ }
+
+ if constexpr (std::is_same_v<T, i64>) {
+ consumer->OnDouble(time, value / options.RateDenominator);
+ } else {
+ consumer->OnDouble(time, value.SecondsFloat() / options.RateDenominator);
+ }
+ } else {
+ // TODO(prime@): RATE is incompatible with windowed read.
+ if constexpr (std::is_same_v<T, i64>) {
+ consumer->OnInt64(time, Rollup(*window, indices.back()));
+ } else {
+ consumer->OnDouble(time, Rollup(*window, indices.back()).SecondsFloat());
+ }
+ }
+ });
+
+ consumer->OnMetricEnd();
+ } else if constexpr (std::is_same_v<T, double>) {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::GAUGE);
+
+ writeLabels(tagIds, nameLabel, true);
+
+ rangeValues([&, window=&window] (auto /* value */, auto time, const auto& indices) {
+ if (options.DisableDefault && !window->HasValue[indices.back()]) {
+ return;
+ }
+
+ sensorCount += 1;
+ consumer->OnDouble(time, window->Values[indices.back()]);
+ });
+
+ consumer->OnMetricEnd();
+ } else if constexpr (std::is_same_v<T, TSummarySnapshot<double>>) {
+ writeSummary([] (auto value) {
+ return MakeIntrusive<NMonitoring::TSummaryDoubleSnapshot>(
+ value.Sum(),
+ value.Min(),
+ value.Max(),
+ value.Last(),
+ static_cast<ui64>(value.Count()));
+ });
+ } else if constexpr (std::is_same_v<T, TSummarySnapshot<TDuration>>) {
+ writeSummary([] (auto value) {
+ return MakeIntrusive<NMonitoring::TSummaryDoubleSnapshot>(
+ value.Sum().SecondsFloat(),
+ value.Min().SecondsFloat(),
+ value.Max().SecondsFloat(),
+ value.Last().SecondsFloat(),
+ static_cast<ui64>(value.Count()));
+ });
+ } else if constexpr (std::is_same_v<T, TTimeHistogramSnapshot>) {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::HIST);
+
+ writeLabels(tagIds, nameLabel, true);
+
+ rangeValues([&, window=&window] (auto value, auto time, const auto& indices) {
+ size_t n = value.Bounds.size();
+ auto hist = NMonitoring::TExplicitHistogramSnapshot::New(n + 1);
+
+ if (options.ConvertCountersToRateGauge || options.EnableHistogramCompat) {
+ if (options.RateDenominator < 0.1) {
+ THROW_ERROR_EXCEPTION("Invalid rate denominator");
+ }
+
+ for (size_t i = 0; i < n; ++i) {
+ int bucketValue = i < value.Values.size() ? value.Values[i] : 0u;
+
+ (*hist)[i] = {value.Bounds[i], bucketValue / options.RateDenominator};
+ }
+
+ // Add inf.
+ (*hist)[n] = {Max<NMonitoring::TBucketBound>(), n < value.Values.size() ? (value.Values[n] / options.RateDenominator) : 0u};
+ } else {
+ auto rollup = Rollup(*window, indices.back());
+
+ for (size_t i = 0; i < n; ++i) {
+ int bucketValue = i < rollup.Values.size() ? rollup.Values[i] : 0u;
+ (*hist)[i] = {rollup.Bounds[i], bucketValue};
+ }
+
+ // Add inf.
+ (*hist)[n] = {Max<NMonitoring::TBucketBound>(), n < rollup.Values.size() ? (rollup.Values[n]) : 0u};
+ }
+
+ sensorCount = n + 1;
+
+ consumer->OnHistogram(time, hist);
+ });
+
+ consumer->OnMetricEnd();
+ } else if constexpr (std::is_same_v<T, TGaugeHistogramSnapshot>) {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::HIST);
+
+ writeLabels(tagIds, nameLabel, true);
+
+ rangeValues([&] (auto value, auto time, const auto& /*indices*/) {
+ size_t n = value.Bounds.size();
+ auto hist = NMonitoring::TExplicitHistogramSnapshot::New(n + 1);
+
+ for (size_t i = 0; i < n; ++i) {
+ int bucketValue = i < value.Values.size() ? value.Values[i] : 0u;
+ (*hist)[i] = {value.Bounds[i], bucketValue};
+ }
+
+ // Add inf.
+ (*hist)[n] = {Max<NMonitoring::TBucketBound>(), n < value.Values.size() ? value.Values[n] : 0u};
+
+ sensorCount = n + 1;
+
+ consumer->OnHistogram(time, hist);
+ });
+
+ consumer->OnMetricEnd();
+ } else if constexpr (std::is_same_v<T, TRateHistogramSnapshot>) {
+ consumer->OnMetricBegin(NMonitoring::EMetricType::HIST);
+
+ writeLabels(tagIds, nameLabel, true);
+
+ rangeValues([&] (auto value, auto time, const auto& /*indices*/) {
+ size_t n = value.Bounds.size();
+ auto hist = NMonitoring::TExplicitHistogramSnapshot::New(n + 1);
+
+ if (options.RateDenominator < 0.1) {
+ THROW_ERROR_EXCEPTION("Invalid rate denominator");
+ }
+
+ for (size_t i = 0; i < n; ++i) {
+ int bucketValue = i < value.Values.size() ? value.Values[i] : 0u;
+ (*hist)[i] = {value.Bounds[i], bucketValue / options.RateDenominator};
+ }
+
+ // Add inf.
+ (*hist)[n] = {Max<NMonitoring::TBucketBound>(), n < value.Values.size() ? (value.Values[n] / options.RateDenominator) : 0u};
+
+ sensorCount = n + 1;
+
+ consumer->OnHistogram(time, hist);
+ });
+
+ consumer->OnMetricEnd();
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected cube type");
+ }
+
+ sensorsEmitted += sensorCount;
+ }
+
+ return sensorsEmitted;
+}
+
+template <class T>
+int TCube<T>::ReadSensorValues(
+ const TTagIdList& tagIds,
+ int index,
+ const TReadOptions& options,
+ const TTagRegistry& tagRegistry,
+ TFluentAny fluent) const
+{
+ int valuesRead = 0;
+ auto doReadValueForProjection = [&] (TFluentAny fluent, const TProjection& projection, const T& value) {
+ if constexpr (std::is_same_v<T, i64> || std::is_same_v<T, TDuration>) {
+ // NB(eshcherbin): Not much sense in returning rate here.
+ if constexpr (std::is_same_v<T, i64>) {
+ fluent.Value(Rollup(projection, index));
+ } else {
+ fluent.Value(Rollup(projection, index).SecondsFloat());
+ }
+ ++valuesRead;
+ } else if constexpr (std::is_same_v<T, double>) {
+ fluent.Value(value);
+ ++valuesRead;
+ } else if constexpr (std::is_same_v<T, TSummarySnapshot<double>>) {
+ if (options.ExportSummaryAsMax && options.SummaryAsMaxForAllTime) {
+ fluent
+ .BeginMap()
+ .Item("max").Value(value.Max())
+ .Item("all_time_max").Value(Rollup(projection, index).Max())
+ .EndMap();
+ } else if (options.ExportSummaryAsMax) {
+ fluent.Value(value.Max());
+ } else {
+ fluent
+ .BeginMap()
+ .Item("sum").Value(value.Sum())
+ .Item("min").Value(value.Min())
+ .Item("max").Value(value.Max())
+ .Item("last").Value(value.Last())
+ .Item("count").Value(static_cast<ui64>(value.Count()))
+ .EndMap();
+ }
+ ++valuesRead;
+ } else if constexpr (std::is_same_v<T, TSummarySnapshot<TDuration>>) {
+ if (options.ExportSummaryAsMax && options.SummaryAsMaxForAllTime) {
+ fluent
+ .BeginMap()
+ .Item("max").Value(value.Max().SecondsFloat())
+ .Item("all_time_max").Value(Rollup(projection, index).Max().SecondsFloat())
+ .EndMap();
+ } else if (options.ExportSummaryAsMax) {
+ fluent.Value(value.Max().SecondsFloat());
+ } else {
+ fluent
+ .BeginMap()
+ .Item("sum").Value(value.Sum().SecondsFloat())
+ .Item("min").Value(value.Min().SecondsFloat())
+ .Item("max").Value(value.Max().SecondsFloat())
+ .Item("last").Value(value.Last().SecondsFloat())
+ .Item("count").Value(static_cast<ui64>(value.Count()))
+ .EndMap();
+ }
+ ++valuesRead;
+ } else if constexpr (std::is_same_v<T, TTimeHistogramSnapshot> || std::is_same_v<T, TGaugeHistogramSnapshot> || std::is_same_v<T, TRateHistogramSnapshot>) {
+ std::vector<std::pair<double, int>> hist;
+ size_t n = value.Bounds.size();
+ hist.reserve(n + 1);
+ for (size_t i = 0; i != n; ++i) {
+ int bucketValue = i < value.Values.size() ? value.Values[i] : 0;
+ hist.emplace_back(value.Bounds[i], bucketValue);
+ }
+ hist.emplace_back(Max<double>(), n < value.Values.size() ? value.Values[n] : 0u);
+
+ fluent.DoListFor(hist, [] (TFluentList fluent, const auto& bar) {
+ fluent
+ .Item().BeginMap()
+ .Item("bound").Value(bar.first)
+ .Item("count").Value(bar.second)
+ .EndMap();
+ });
+ ++valuesRead;
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected cube type");
+ }
+ };
+
+ // NB(eshcherbin): ReadAllProjections is intended only for debugging purposes.
+ if (options.ReadAllProjections) {
+ std::vector<TTagIdList> filteredProjectionTagIds;
+ for (const auto& [projectionTagIds, _] : Projections_) {
+ // NB(eshcherbin): All tagIds vector are guaranteed to be sorted.
+ if (std::includes(projectionTagIds.begin(), projectionTagIds.end(), tagIds.begin(), tagIds.end())) {
+ filteredProjectionTagIds.push_back(projectionTagIds);
+ }
+ }
+
+ if (!filteredProjectionTagIds.empty()) {
+ fluent.DoListFor(filteredProjectionTagIds, [&] (TFluentList fluent, const auto& projectionTagIds) {
+ const auto& projection = GetOrCrash(Projections_, projectionTagIds);
+
+ fluent
+ .Item().BeginMap()
+ .Item("tags").DoMapFor(projectionTagIds, [&] (TFluentMap fluent, auto tagId) {
+ const auto& [key, value] = tagRegistry.Decode(tagId);
+ fluent.Item(key).Value(value);
+ })
+ .Item("value").Do([&] (TFluentAny fluent) {
+ doReadValueForProjection(fluent, projection, projection.Values[index]);
+ })
+ .EndMap();
+ });
+ }
+ } else {
+ auto it = Projections_.find(tagIds);
+ if (it == Projections_.end()) {
+ return valuesRead;
+ }
+
+ const auto& projection = it->second;
+ doReadValueForProjection(fluent, projection, projection.Values[index]);
+ }
+
+ return valuesRead;
+}
+
+template <class T>
+void TCube<T>::DumpCube(NProto::TCube *cube) const
+{
+ for (const auto& [tagIds, window] : Projections_) {
+ auto projection = cube->add_projections();
+ for (auto tagId : tagIds) {
+ projection->add_tag_ids(tagId);
+ }
+
+ projection->set_has_value(window.HasValue[Index_]);
+ if constexpr (std::is_same_v<T, i64>) {
+ projection->set_counter(window.Values[Index_]);
+ } else if constexpr (std::is_same_v<T, TDuration>) {
+ projection->set_duration(window.Values[Index_].GetValue());
+ } else if constexpr (std::is_same_v<T, double>) {
+ projection->set_gauge(window.Values[Index_]);
+ } else if constexpr (std::is_same_v<T, TSummarySnapshot<double>>) {
+ ToProto(projection->mutable_summary(), window.Values[Index_]);
+ } else if constexpr (std::is_same_v<T, TSummarySnapshot<TDuration>>) {
+ ToProto(projection->mutable_timer(), window.Values[Index_]);
+ } else if constexpr (std::is_same_v<T, TTimeHistogramSnapshot>) {
+ ToProto(projection->mutable_time_histogram(), window.Values[Index_]);
+ } else if constexpr (std::is_same_v<T, TGaugeHistogramSnapshot>) {
+ ToProto(projection->mutable_gauge_histogram(), window.Values[Index_]);
+ } else if constexpr (std::is_same_v<T, TRateHistogramSnapshot>) {
+ ToProto(projection->mutable_rate_histogram(), window.Values[Index_]);
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected cube type");
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template class TCube<double>;
+template class TCube<i64>;
+template class TCube<TDuration>;
+template class TCube<TSummarySnapshot<double>>;
+template class TCube<TSummarySnapshot<TDuration>>;
+template class TCube<TTimeHistogramSnapshot>;
+template class TCube<TGaugeHistogramSnapshot>;
+template class TCube<TRateHistogramSnapshot>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/cube.h b/yt/yt/library/profiling/solomon/cube.h
new file mode 100644
index 0000000000..ab0006c029
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/cube.h
@@ -0,0 +1,132 @@
+#pragma once
+
+#include "private.h"
+#include "tag_registry.h"
+
+#include <limits>
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/summary.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <library/cpp/monlib/metrics/metric_consumer.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+typedef std::vector<std::pair<std::vector<int>, TInstant>> TReadWindow;
+
+struct TReadOptions
+{
+ TReadWindow Times;
+
+ std::function<bool(const TString&)> SensorFilter;
+
+ bool ConvertCountersToRateGauge = false;
+ bool RenameConvertedCounters = true;
+ double RateDenominator = 1.0;
+ bool EnableHistogramCompat = false;
+
+ bool EnableSolomonAggregationWorkaround = false;
+
+ // Direct summary export is not supported by solomon, yet.
+ bool ExportSummary = false;
+ bool ExportSummaryAsMax = false;
+ bool ExportSummaryAsAvg = false;
+
+ bool MarkAggregates = false;
+
+ std::optional<TString> Host;
+
+ std::vector<TTag> InstanceTags;
+
+ bool Sparse = false;
+ bool Global = false;
+ bool DisableSensorsRename = false;
+ bool DisableDefault = false;
+
+ int LingerWindowSize = 0;
+
+ // Used only in ReadRecentSensorValue.
+ bool ReadAllProjections = false;
+
+ // Only makes sense with ExportSummaryAsMax and ReadAllProjections.
+ bool SummaryAsMaxForAllTime = false;
+
+ // Drop all prefix before last '/'.
+ bool StripSensorsNamePrefix = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+bool IsZeroValue(const T& value)
+{
+ T zeroValue{};
+ return value == zeroValue;
+}
+
+template <class T>
+class TCube
+{
+public:
+ TCube(int windowSize, i64 nextIteration);
+
+ void Add(TTagIdList tagIds);
+ void AddAll(const TTagIdList& tagIds, const TProjectionSet& projections);
+ void Remove(TTagIdList tagIds);
+ void RemoveAll(const TTagIdList& tagIds, const TProjectionSet& projections);
+
+ void Update(TTagIdList tagIds, T value);
+ void StartIteration();
+ void FinishIteration();
+
+ struct TProjection
+ {
+ T Rollup{};
+ std::vector<T> Values;
+ std::vector<bool> HasValue;
+
+ bool IsZero(int index) const;
+ bool IsLingering(i64 iteration) const;
+
+ i64 LastNonZeroIteration = std::numeric_limits<i64>::min();
+ int UsageCount = 0;
+ };
+
+ const THashMap<TTagIdList, TCube::TProjection>& GetProjections() const;
+ int GetSize() const;
+
+ int GetIndex(i64 iteration) const;
+ i64 GetIteration(int index) const;
+ T Rollup(const TProjection& window, int index) const;
+
+ int ReadSensors(
+ const TString& name,
+ const TReadOptions& options,
+ TTagWriter* tagWriter,
+ ::NMonitoring::IMetricConsumer* consumer) const;
+
+ int ReadSensorValues(
+ const TTagIdList& tagIds,
+ int index,
+ const TReadOptions& options,
+ const TTagRegistry& tagRegistry,
+ NYTree::TFluentAny fluent) const;
+
+ void DumpCube(NProto::TCube* cube) const;
+
+private:
+ const int WindowSize_;
+
+ i64 NextIteration_;
+ i64 BaseIteration_;
+ int Index_ = 0;
+
+ THashMap<TTagIdList, TProjection> Projections_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/exporter.cpp b/yt/yt/library/profiling/solomon/exporter.cpp
new file mode 100644
index 0000000000..e0904903b1
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/exporter.cpp
@@ -0,0 +1,1005 @@
+#include "exporter.h"
+#include "private.h"
+#include "sensor_service.h"
+
+#include <yt/yt/build/build.h>
+
+#include <yt/yt/core/http/http.h>
+#include <yt/yt/core/http/server.h>
+#include <yt/yt/core/http/helpers.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <library/cpp/monlib/encode/json/json.h>
+#include <library/cpp/monlib/encode/spack/spack_v1.h>
+#include <library/cpp/monlib/encode/prometheus/prometheus.h>
+
+#include <library/cpp/cgiparam/cgiparam.h>
+
+#include <util/datetime/base.h>
+
+#include <util/stream/str.h>
+
+namespace NYT::NProfiling {
+
+using namespace NConcurrency;
+using namespace NHttp;
+using namespace NYTree;
+using namespace NLogging;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const static auto& Logger = SolomonLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TShardConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("filter", &TThis::Filter)
+ .Default();
+
+ registrar.Parameter("grid_step", &TThis::GridStep)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSolomonExporterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("grid_step", &TThis::GridStep)
+ .Default(TDuration::Seconds(5));
+
+ registrar.Parameter("linger_timeout", &TThis::LingerTimeout)
+ .Default(TDuration::Minutes(5));
+
+ registrar.Parameter("window_size", &TThis::WindowSize)
+ .Default(12);
+
+ registrar.Parameter("thread_pool_size", &TThis::ThreadPoolSize)
+ .Default(1);
+
+ registrar.Parameter("convert_counters_to_rate_for_solomon", &TThis::ConvertCountersToRateForSolomon)
+ .Alias("convert_counters_to_rate")
+ .Default(true);
+ registrar.Parameter("rename_converted_counters", &TThis::RenameConvertedCounters)
+ .Default(true);
+
+ registrar.Parameter("export_summary", &TThis::ExportSummary)
+ .Default(false);
+ registrar.Parameter("export_summary_as_max", &TThis::ExportSummaryAsMax)
+ .Default(true);
+ registrar.Parameter("export_summary_as_avg", &TThis::ExportSummaryAsAvg)
+ .Default(false);
+
+ registrar.Parameter("mark_aggregates", &TThis::MarkAggregates)
+ .Default(true);
+
+ registrar.Parameter("strip_sensors_name_prefix", &TThis::StripSensorsNamePrefix)
+ .Default(false);
+
+ registrar.Parameter("enable_self_profiling", &TThis::EnableSelfProfiling)
+ .Default(true);
+
+ registrar.Parameter("report_build_info", &TThis::ReportBuildInfo)
+ .Default(true);
+
+ registrar.Parameter("report_kernel_version", &TThis::ReportKernelVersion)
+ .Default(true);
+
+ registrar.Parameter("report_restart", &TThis::ReportRestart)
+ .Default(true);
+
+ registrar.Parameter("read_delay", &TThis::ReadDelay)
+ .Default(TDuration::Seconds(5));
+
+ registrar.Parameter("host", &TThis::Host)
+ .Default();
+
+ registrar.Parameter("instance_tags", &TThis::InstanceTags)
+ .Default();
+
+ registrar.Parameter("shards", &TThis::Shards)
+ .Default();
+
+ registrar.Parameter("response_cache_ttl", &TThis::ResponseCacheTtl)
+ .Default(TDuration::Minutes(2));
+
+ registrar.Parameter("update_sensor_service_tree_period", &TThis::UpdateSensorServiceTreePeriod)
+ .Default(TDuration::Seconds(30));
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->LingerTimeout.GetValue() % config->GridStep.GetValue() != 0) {
+ THROW_ERROR_EXCEPTION("\"linger_timeout\" must be multiple of \"grid_step\"");
+ }
+ });
+
+ registrar.Postprocessor([] (TThis* config) {
+ for (const auto& [name, shard] : config->Shards) {
+ if (!shard->GridStep) {
+ continue;
+ }
+
+ if (shard->GridStep < config->GridStep) {
+ THROW_ERROR_EXCEPTION("shard \"grid_step\" must be greater than global \"grid_step\"");
+ }
+
+ if (shard->GridStep->GetValue() % config->GridStep.GetValue() != 0) {
+ THROW_ERROR_EXCEPTION("shard \"grid_step\" must be multiple of global \"grid_step\"");
+ }
+
+ if (config->LingerTimeout.GetValue() % shard->GridStep->GetValue() != 0) {
+ THROW_ERROR_EXCEPTION("\"linger_timeout\" must be multiple shard \"grid_step\"");
+ }
+ }
+ });
+}
+
+TShardConfigPtr TSolomonExporterConfig::MatchShard(const TString& sensorName)
+{
+ TShardConfigPtr matchedShard;
+ int matchSize = -1;
+
+ for (const auto& [name, config] : Shards) {
+ for (auto prefix : config->Filter) {
+ if (!sensorName.StartsWith(prefix)) {
+ continue;
+ }
+
+ if (static_cast<int>(prefix.size()) > matchSize) {
+ matchSize = prefix.size();
+ matchedShard = config;
+ }
+ }
+ }
+
+ return matchedShard;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSolomonExporter::TSolomonExporter(
+ TSolomonExporterConfigPtr config,
+ TSolomonRegistryPtr registry)
+ : Config_(std::move(config))
+ , Registry_(registry ? registry : TSolomonRegistry::Get())
+ , ControlQueue_(New<TActionQueue>("ProfControl"))
+ , OffloadThreadPool_(CreateThreadPool(Config_->ThreadPoolSize, "ProfOffload"))
+{
+ if (Config_->EnableSelfProfiling) {
+ Registry_->Profile(TProfiler{Registry_, ""});
+ }
+
+ CollectionStartDelay_ = Registry_->GetSelfProfiler().Timer("/collection_delay");
+ WindowErrors_ = Registry_->GetSelfProfiler().Counter("/window_error_count");
+ ReadDelays_ = Registry_->GetSelfProfiler().Counter("/read_delay_count");
+ ResponseCacheMiss_ = Registry_->GetSelfProfiler().Counter("/response_cache_miss");
+ ResponseCacheHit_ = Registry_->GetSelfProfiler().Counter("/response_cache_hit");
+
+ for (const auto& [name, config] : Config_->Shards) {
+ LastShardFetch_[name] = std::nullopt;
+ }
+
+ Registry_->SetWindowSize(Config_->WindowSize);
+ Registry_->SetGridFactor([config = Config_] (const TString& name) -> int {
+ auto shard = config->MatchShard(name);
+ if (!shard) {
+ return 1;
+ }
+
+ if (!shard->GridStep) {
+ return 1;
+ }
+
+ return shard->GridStep->GetValue() / config->GridStep.GetValue();
+ });
+
+ if (Config_->ReportBuildInfo) {
+ TProfiler profiler{registry, ""};
+
+ profiler
+ .WithRequiredTag("version", GetVersion())
+ .AddFuncGauge("/build/version", MakeStrong(this), [] { return 1.0; });
+ }
+
+ if (Config_->ReportKernelVersion) {
+ TProfiler profiler{registry, ""};
+
+ profiler
+ .WithRequiredTag("kernel_version", GetLinuxKernelVersion())
+ .AddFuncGauge("/host/kernel_version", MakeStrong(this), [] { return 1.0; });
+ }
+
+ if (Config_->ReportRestart) {
+ TProfiler profiler{registry, ""};
+
+ for (auto window : {1, 5, 30}) {
+ profiler
+ .WithRequiredTag("window", ToString(window) + "min")
+ .AddFuncGauge("/server/restarted", MakeStrong(this), [this, window] {
+ return (TInstant::Now() - StartTime_ < TDuration::Minutes(window)) ? 1.0 : 0.0;
+ });
+ }
+ }
+}
+
+void TSolomonExporter::Register(const TString& prefix, const NYT::NHttp::IServerPtr& server)
+{
+ Register(prefix, server->GetPathMatcher());
+}
+
+void TSolomonExporter::Register(const TString& prefix, const NYT::NHttp::IRequestPathMatcherPtr& handlers)
+{
+ handlers->Add(prefix + "/", BIND(&TSolomonExporter::HandleIndex, MakeStrong(this), prefix));
+ handlers->Add(prefix + "/sensors", BIND(&TSolomonExporter::HandleDebugSensors, MakeStrong(this)));
+ handlers->Add(prefix + "/tags", BIND(&TSolomonExporter::HandleDebugTags, MakeStrong(this)));
+ handlers->Add(prefix + "/status", BIND(&TSolomonExporter::HandleStatus, MakeStrong(this)));
+ handlers->Add(prefix + "/all", BIND(&TSolomonExporter::HandleShard, MakeStrong(this), std::nullopt));
+
+ for (const auto& [shardName, shard] : Config_->Shards) {
+ handlers->Add(
+ prefix + "/shard/" + shardName,
+ BIND(&TSolomonExporter::HandleShard, MakeStrong(this), shardName));
+ }
+}
+
+void TSolomonExporter::Start()
+{
+ CollectorFuture_ = BIND([this, this_ = MakeStrong(this)] {
+ try {
+ DoCollect();
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Sensor collector crashed");
+ }
+ })
+ .AsyncVia(ControlQueue_->GetInvoker())
+ .Run();
+}
+
+void TSolomonExporter::Stop()
+{
+ CollectorFuture_.Cancel(TError("Stopped"));
+ ControlQueue_->Shutdown();
+ OffloadThreadPool_->Shutdown();
+}
+
+void TSolomonExporter::TransferSensors()
+{
+ std::vector<std::pair<TFuture<TSharedRef>, TIntrusivePtr<TRemoteProcess>>> remoteFutures;
+ {
+ auto processGuard = Guard(RemoteProcessLock_);
+
+ for (const auto& process : RemoteProcessList_) {
+ try {
+ auto asyncDump = process->DumpSensors();
+ remoteFutures.emplace_back(asyncDump, process);
+ } catch (const std::exception& ex) {
+ remoteFutures.emplace_back(MakeFuture<TSharedRef>(TError(ex)), process);
+ }
+ }
+ }
+
+ std::vector<TIntrusivePtr<TRemoteProcess>> deadProcesses;
+ for (const auto& [dumpFuture, process] : remoteFutures) {
+ // Use blocking Get(), because we want to lock current thread while data structure is updating.
+ auto result = dumpFuture.Get();
+
+ if (result.IsOK()) {
+ try {
+ NProto::TSensorDump sensorDump;
+ DeserializeProto(&sensorDump, result.Value());
+ process->Registry.Transfer(sensorDump);
+ } catch (const std::exception& ex) {
+ result = TError(ex);
+ }
+ }
+
+ if (!result.IsOK()) {
+ process->Registry.Detach();
+ deadProcesses.push_back(process);
+ }
+ }
+
+ {
+ auto processGuard = Guard(RemoteProcessLock_);
+ for (const auto& process : deadProcesses) {
+ RemoteProcessList_.erase(process);
+ }
+ }
+}
+
+void TSolomonExporter::DoCollect()
+{
+ auto nowUnix = TInstant::Now().GetValue();
+ nowUnix -= (nowUnix % Config_->GridStep.GetValue());
+ auto nextGridTime = TInstant::FromValue(nowUnix) + Config_->GridStep;
+
+ auto waitUntil = [] (auto deadline) {
+ auto now = TInstant::Now();
+ if (now >= deadline) {
+ Yield();
+ return;
+ }
+ TDelayedExecutor::WaitForDuration(deadline - now);
+ };
+
+ // Compute start time. Zero iteration time should be aligned with each GridStep.
+ while (true) {
+ bool aligned = true;
+ for (const auto& [name, shard] : Config_->Shards) {
+ if (shard->GridStep && nextGridTime.GetValue() % shard->GridStep->GetValue() != 0) {
+ aligned = false;
+ }
+ }
+
+ if (aligned) {
+ break;
+ }
+
+ nextGridTime += Config_->GridStep;
+ }
+
+ YT_LOG_DEBUG("Sensor collector started (StartTime: %v)", nextGridTime);
+ while (true) {
+ CleanResponseCache();
+
+ waitUntil(nextGridTime);
+
+ auto guard = WaitFor(TAsyncLockWriterGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ auto delay = TInstant::Now() - nextGridTime;
+ CollectionStartDelay_.Record(delay);
+
+ YT_LOG_DEBUG("Started sensor collection (Delay: %v)", delay);
+ Registry_->ProcessRegistrations();
+
+ auto iteration = Registry_->GetNextIteration();
+ Registry_->Collect(OffloadThreadPool_->GetInvoker());
+
+ TransferSensors();
+
+ Window_.emplace_back(iteration, nextGridTime);
+ if (Window_.size() > static_cast<size_t>(Registry_->GetWindowSize())) {
+ Window_.erase(Window_.begin());
+ }
+
+ YT_LOG_DEBUG("Finished sensor collection (Delay: %v)", TInstant::Now() - nextGridTime);
+ nextGridTime += Config_->GridStep;
+ }
+}
+
+constexpr auto IndexPage = R"EOF(
+<!DOCTYPE html>
+<html>
+<body>
+<a href="sensors">sensors top</a>
+<br/>
+<a href="tags">tags top</a>
+<br/>
+<a href="status">status</a>
+</body>
+</html>
+)EOF";
+
+void TSolomonExporter::HandleIndex(const TString& prefix, const IRequestPtr& req, const IResponseWriterPtr& rsp)
+{
+ if (req->GetUrl().Path != prefix && req->GetUrl().Path != prefix + "/") {
+ rsp->SetStatus(EStatusCode::NotFound);
+ WaitFor(rsp->WriteBody(TSharedRef::FromString("Not found")))
+ .ThrowOnError();
+ return;
+ }
+
+ rsp->SetStatus(EStatusCode::OK);
+ rsp->GetHeaders()->Add("Content-Type", "text/html; charset=UTF-8");
+
+ WaitFor(rsp->WriteBody(TSharedRef::FromString(IndexPage)))
+ .ThrowOnError();
+}
+
+void TSolomonExporter::HandleStatus(const IRequestPtr&, const IResponseWriterPtr& rsp)
+{
+ auto guard = WaitFor(TAsyncLockReaderGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ auto sensors = Registry_->ListSensors();
+ THashMap<TString, TError> invalidSensors;
+ for (const auto& sensor : sensors) {
+ if (sensor.Error.IsOK()) {
+ continue;
+ }
+
+ invalidSensors[sensor.Name] = sensor.Error;
+ }
+
+ rsp->SetStatus(EStatusCode::OK);
+ ReplyJson(rsp, [&] (auto consumer) {
+ auto statusGuard = Guard(StatusLock_);
+
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("start_time").Value(StartTime_)
+ .Item("last_fetch").Value(LastFetch_)
+ .Item("last_shard_fetch").DoMapFor(LastShardFetch_, [] (auto fluent, auto time) {
+ fluent
+ .Item(time.first).Value(time.second);
+ })
+ .Item("window").Value(Window_)
+ .Item("sensor_errors").DoMapFor(invalidSensors, [] (auto fluent, auto sensor) {
+ fluent
+ .Item(sensor.first).Value(sensor.second);
+ })
+ .Item("dynamic_tags").Value(Registry_->GetDynamicTags())
+ .EndMap();
+ });
+}
+
+void TSolomonExporter::HandleDebugSensors(const IRequestPtr&, const IResponseWriterPtr& rsp)
+{
+ auto guard = WaitFor(TAsyncLockReaderGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ rsp->SetStatus(EStatusCode::OK);
+ rsp->GetHeaders()->Add("Content-Type", "text/plain; charset=UTF-8");
+
+ auto sensors = Registry_->ListSensors();
+ std::sort(sensors.begin(), sensors.end(), [] (const auto& a, const auto& b) {
+ return std::tie(a.CubeSize, a.Name) > std::tie(b.CubeSize, b.Name);
+ });
+
+ TStringStream out;
+ out << "# cube_size object_count name error" << Endl;
+ for (const auto& sensor : sensors) {
+ out << sensor.CubeSize << " " << sensor.ObjectCount << " " << sensor.Name;
+
+ if (!sensor.Error.IsOK()) {
+ out << " " << ToString(sensor.Error);
+ }
+
+ out << Endl;
+ }
+
+ WaitFor(rsp->WriteBody(TSharedRef::FromString(out.Str())))
+ .ThrowOnError();
+}
+
+void TSolomonExporter::HandleDebugTags(const IRequestPtr&, const IResponseWriterPtr& rsp)
+{
+ auto guard = WaitFor(TAsyncLockReaderGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ rsp->SetStatus(EStatusCode::OK);
+ rsp->GetHeaders()->Add("Content-Type", "text/plain; charset=UTF-8");
+
+ auto tags = Registry_->GetTags().GetTopByKey();
+ std::vector<std::pair<TString, size_t>> tagList{tags.begin(), tags.end()};
+ std::sort(tagList.begin(), tagList.end(), [] (auto a, auto b) {
+ return std::tie(a.second, a.first) > std::tie(b.second, b.first);
+ });
+
+ TStringStream out;
+ out << "# value_count tag_name" << Endl;
+ for (const auto& tag : tagList) {
+ out << tag.second << " " << tag.first << Endl;
+ }
+
+ WaitFor(rsp->WriteBody(TSharedRef::FromString(out.Str())))
+ .ThrowOnError();
+}
+
+std::optional<TString> TSolomonExporter::ReadJson(const TReadOptions& options, std::optional<TString> shard)
+{
+ TStringStream buffer;
+ auto encoder = NMonitoring::BufferedEncoderJson(&buffer);
+ if (ReadSensors(std::move(encoder), options, shard)) {
+ return buffer.Str();
+ }
+ return {};
+}
+
+std::optional<TString> TSolomonExporter::ReadSpack(const TReadOptions& options, std::optional<TString> shard)
+{
+ TStringStream buffer;
+ auto encoder = NMonitoring::EncoderSpackV1(
+ &buffer,
+ NMonitoring::ETimePrecision::SECONDS,
+ NMonitoring::ECompression::ZSTD);
+ if (ReadSensors(std::move(encoder), options, shard)) {
+ return buffer.Str();
+ }
+ return {};
+}
+
+bool TSolomonExporter::ReadSensors(
+ NMonitoring::IMetricEncoderPtr encoder,
+ const TReadOptions& options,
+ std::optional<TString> shard)
+{
+ auto guard = WaitFor(TAsyncLockReaderGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ if (Window_.empty()) {
+ return false;
+ }
+
+ // Read last value.
+ auto readOptions = options;
+ readOptions.Times.emplace_back(std::vector<int>{Registry_->IndexOf(Window_.back().first)}, TInstant::Zero());
+ readOptions.ConvertCountersToRateGauge = false;
+ readOptions.EnableHistogramCompat = true;
+ readOptions.ExportSummary |= Config_->ExportSummary;
+ readOptions.ExportSummaryAsMax |= Config_->ExportSummaryAsMax;
+ readOptions.ExportSummaryAsAvg |= Config_->ExportSummaryAsAvg;
+ readOptions.MarkAggregates |= Config_->MarkAggregates;
+ if (!readOptions.Host && Config_->Host) {
+ readOptions.Host = Config_->Host;
+ }
+ if (readOptions.InstanceTags.empty() && !Config_->InstanceTags.empty()) {
+ readOptions.InstanceTags.reserve(Config_->InstanceTags.size());
+ for (auto&& [k, v] : Config_->InstanceTags) {
+ readOptions.InstanceTags.emplace_back(k, v);
+ }
+ }
+
+ if (shard) {
+ readOptions.SensorFilter = [&, name = shard.value()] (const TString& sensorName) {
+ return Config_->MatchShard(sensorName) == Config_->Shards[name];
+ };
+ } else {
+ readOptions.SensorFilter = [this] (const TString& sensorName) {
+ return FilterDefaultGrid(sensorName);
+ };
+ }
+
+ encoder->OnStreamBegin();
+ Registry_->ReadSensors(readOptions, encoder.Get());
+ encoder->OnStreamEnd();
+ guard->Release();
+ encoder->Close();
+ return true;
+}
+
+void TSolomonExporter::HandleShard(
+ const std::optional<TString>& name,
+ const IRequestPtr& req,
+ const IResponseWriterPtr& rsp)
+{
+ auto reply = BIND(&TSolomonExporter::DoHandleShard, MakeStrong(this), name, req, rsp)
+ .AsyncVia(OffloadThreadPool_->GetInvoker())
+ .Run();
+
+ WaitFor(reply)
+ .ThrowOnError();
+}
+
+void TSolomonExporter::DoHandleShard(
+ const std::optional<TString>& name,
+ const IRequestPtr& req,
+ const IResponseWriterPtr& rsp)
+{
+ TPromise<TSharedRef> responsePromise = NewPromise<TSharedRef>();
+
+ try {
+ auto format = NMonitoring::EFormat::JSON;
+ if (auto accept = req->GetHeaders()->Find("Accept")) {
+ format = NMonitoring::FormatFromAcceptHeader(*accept);
+ }
+
+ NMonitoring::ECompression compression = NMonitoring::ECompression::IDENTITY;
+ if (auto acceptEncoding = req->GetHeaders()->Find("Accept-Encoding")) {
+ compression = NMonitoring::CompressionFromAcceptEncodingHeader(*acceptEncoding);
+ }
+
+ TStringStream buffer;
+
+ NMonitoring::IMetricEncoderPtr encoder;
+ switch (format) {
+ case NMonitoring::EFormat::UNKNOWN:
+ case NMonitoring::EFormat::JSON:
+ encoder = NMonitoring::BufferedEncoderJson(&buffer);
+ format = NMonitoring::EFormat::JSON;
+ compression = NMonitoring::ECompression::IDENTITY;
+ break;
+
+ case NMonitoring::EFormat::SPACK:
+ encoder = NMonitoring::EncoderSpackV1(
+ &buffer,
+ NMonitoring::ETimePrecision::SECONDS,
+ compression);
+ break;
+
+ case NMonitoring::EFormat::PROMETHEUS:
+ encoder = NMonitoring::EncoderPrometheus(&buffer);
+ break;
+
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported format %Qv", NMonitoring::ContentTypeByFormat(format));
+ }
+
+ rsp->GetHeaders()->Set("Content-Type", TString{NMonitoring::ContentTypeByFormat(format)});
+ rsp->GetHeaders()->Set("Content-Encoding", TString{NMonitoring::ContentEncodingByCompression(compression)});
+
+ TCgiParameters params(req->GetUrl().RawQuery);
+
+ std::optional<TDuration> period;
+ if (auto it = params.Find("period"); it != params.end()) {
+ period = TDuration::Parse(it->second);
+ }
+
+ std::optional<TInstant> now;
+ if (auto it = params.Find("now"); it != params.end()) {
+ now = TInstant::ParseIso8601(it->second);
+ }
+
+ std::optional<TDuration> readGridStep;
+ if (auto gridHeader = req->GetHeaders()->Find("X-Solomon-GridSec"); gridHeader) {
+ int gridSeconds;
+ if (!TryFromString<int>(*gridHeader, gridSeconds)) {
+ THROW_ERROR_EXCEPTION("Invalid value of \"X-Solomon-GridSec\" header")
+ << TErrorAttribute("value", *gridHeader);
+ }
+ readGridStep = TDuration::Seconds(gridSeconds);
+ }
+
+ if ((now && !period) || (period && !now)) {
+ THROW_ERROR_EXCEPTION("Both \"period\" and \"now\" must be present in request")
+ << TErrorAttribute("now", now)
+ << TErrorAttribute("period", period);
+ }
+
+ auto gridStep = Config_->GridStep;
+ if (name) {
+ auto shardConfig = Config_->Shards[*name];
+ if (shardConfig->GridStep) {
+ gridStep = *shardConfig->GridStep;
+ }
+ }
+
+ ValidatePeriodAndGrid(period, readGridStep, gridStep);
+
+ auto guard = WaitFor(TAsyncLockReaderGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ if (Window_.empty()) {
+ WindowErrors_.Increment();
+ THROW_ERROR_EXCEPTION("Window is empty");
+ }
+
+ std::optional<TCacheKey> cacheKey;
+ if (now && period) {
+ cacheKey = TCacheKey{
+ .Shard = name,
+ .Format = format,
+ .Compression = compression,
+ .Now = *now,
+ .Period = *period,
+ .Grid = readGridStep,
+ };
+ }
+
+ auto solomonCluster = req->GetHeaders()->Find("X-Solomon-ClusterId");
+ YT_LOG_DEBUG("Processing sensor pull (Format: %v, Compression: %v, SolomonCluster: %v, Now: %v, Period: %v, Grid: %v)",
+ format,
+ compression,
+ solomonCluster ? *solomonCluster : "",
+ now,
+ period,
+ readGridStep);
+
+ if (cacheKey) {
+ auto cacheGuard = Guard(CacheLock_);
+
+ auto cacheHitIt = ResponseCache_.find(*cacheKey);
+ if (cacheHitIt != ResponseCache_.end() && !(cacheHitIt->second.IsSet() && !cacheHitIt->second.Get().IsOK())) {
+ YT_LOG_DEBUG("Replying from cache");
+
+ ResponseCacheHit_.Increment();
+
+ auto cachedResponse = cacheHitIt->second;
+ cacheGuard.Release();
+ guard->Release();
+
+ rsp->SetStatus(EStatusCode::OK);
+ WaitFor(rsp->WriteBody(WaitFor(cachedResponse).ValueOrThrow()))
+ .ThrowOnError();
+
+ return;
+ }
+
+ ResponseCache_[*cacheKey] = responsePromise.ToFuture();
+ }
+
+ TReadWindow readWindow;
+ if (period) {
+ if (auto errorOrWindow = SelectReadWindow(*now, *period, readGridStep, gridStep); !errorOrWindow.IsOK()) {
+ ReadDelays_.Increment();
+ YT_LOG_DEBUG(errorOrWindow, "Delaying sensor read (Delay: %v)", Config_->ReadDelay);
+
+ guard->Release();
+ TDelayedExecutor::WaitForDuration(Config_->ReadDelay);
+ guard = WaitFor(TAsyncLockReaderGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ if (auto errorOrWindow = SelectReadWindow(*now, *period, readGridStep, gridStep); !errorOrWindow.IsOK()) {
+ WindowErrors_.Increment();
+ THROW_ERROR errorOrWindow;
+ } else {
+ readWindow = errorOrWindow.Value();
+ }
+ } else {
+ readWindow = errorOrWindow.Value();
+ }
+ } else {
+ YT_LOG_DEBUG("Timestamp query arguments are missing; returning last value");
+
+ int gridFactor = gridStep / Config_->GridStep;
+ for (auto i = static_cast<int>(Window_.size()) - 1; i >= 0; --i) {
+ auto [iteration, time] = Window_[i];
+ if (iteration % gridFactor == 0) {
+ readWindow.emplace_back(std::vector<int>{Registry_->IndexOf(iteration / gridFactor)}, time);
+ break;
+ }
+ }
+
+ if (readWindow.empty()) {
+ THROW_ERROR_EXCEPTION("Can't find latest timestamp")
+ << TErrorAttribute("first_iteration", Window_[0].first)
+ << TErrorAttribute("grid_factor", gridFactor)
+ << TErrorAttribute("window_size", Window_.size());
+ }
+ }
+
+ if (cacheKey) {
+ ResponseCacheMiss_.Increment();
+ }
+
+ TReadOptions options;
+ options.Host = Config_->Host;
+ options.InstanceTags = std::vector<TTag>{Config_->InstanceTags.begin(), Config_->InstanceTags.end()};
+
+ auto isSolomon = format == NMonitoring::EFormat::JSON || format == NMonitoring::EFormat::SPACK;
+ if (Config_->ConvertCountersToRateForSolomon && isSolomon) {
+ options.ConvertCountersToRateGauge = true;
+ options.RenameConvertedCounters = Config_->RenameConvertedCounters;
+
+ options.RateDenominator = gridStep.SecondsFloat();
+ if (readGridStep) {
+ options.RateDenominator = readGridStep->SecondsFloat();
+ }
+ }
+
+ options.EnableSolomonAggregationWorkaround = isSolomon;
+ options.Times = readWindow;
+ options.ExportSummary = Config_->ExportSummary;
+ options.ExportSummaryAsMax = Config_->ExportSummaryAsMax;
+ options.ExportSummaryAsAvg = Config_->ExportSummaryAsAvg;
+ options.MarkAggregates = Config_->MarkAggregates;
+ options.StripSensorsNamePrefix = Config_->StripSensorsNamePrefix;
+ options.LingerWindowSize = Config_->LingerTimeout / gridStep;
+
+ if (name) {
+ options.SensorFilter = [&] (const TString& sensorName) {
+ return Config_->MatchShard(sensorName) == Config_->Shards[*name];
+ };
+ } else {
+ options.SensorFilter = [this] (const TString& sensorName) {
+ return FilterDefaultGrid(sensorName);
+ };
+ }
+
+ {
+ auto statusGuard = Guard(StatusLock_);
+ if (name) {
+ LastShardFetch_[*name] = TInstant::Now();
+ } else {
+ LastFetch_ = TInstant::Now();
+ }
+ }
+
+ encoder->OnStreamBegin();
+ Registry_->ReadSensors(options, encoder.Get());
+ encoder->OnStreamEnd();
+
+ guard->Release();
+
+ encoder->Close();
+
+ rsp->SetStatus(EStatusCode::OK);
+
+ auto replyBlob = TSharedRef::FromString(buffer.Str());
+ responsePromise.Set(replyBlob);
+
+ WaitFor(rsp->WriteBody(replyBlob))
+ .ThrowOnError();
+ } catch(const std::exception& ex) {
+ YT_LOG_DEBUG(ex, "Failed to export sensors");
+ responsePromise.TrySet(TError(ex));
+
+ if (!rsp->AreHeadersFlushed()) {
+ try {
+ rsp->SetStatus(EStatusCode::InternalServerError);
+ rsp->GetHeaders()->Remove("Content-Type");
+ rsp->GetHeaders()->Remove("Content-Encoding");
+
+ // Send only message. It should be displayed nicely in Solomon UI.
+ WaitFor(rsp->WriteBody(TSharedRef::FromString(TError(ex).GetMessage())))
+ .ThrowOnError();
+ } catch (const std::exception& ex) {
+ YT_LOG_DEBUG(ex, "Failed to send export error");
+ }
+ }
+ }
+}
+
+void TSolomonExporter::ValidatePeriodAndGrid(std::optional<TDuration> period, std::optional<TDuration> readGridStep, TDuration gridStep)
+{
+ if (!period) {
+ return;
+ }
+
+ if (*period < gridStep) {
+ THROW_ERROR_EXCEPTION("Period cannot be lower than grid step")
+ << TErrorAttribute("period", *period)
+ << TErrorAttribute("grid_step", gridStep);
+ }
+
+ if (period->GetValue() % gridStep.GetValue() != 0) {
+ THROW_ERROR_EXCEPTION("Period must be multiple of grid step")
+ << TErrorAttribute("period", *period)
+ << TErrorAttribute("grid_step", gridStep);
+ }
+
+ if (readGridStep) {
+ if (*readGridStep < gridStep) {
+ THROW_ERROR_EXCEPTION("Server grid step cannot be lower than client grid step")
+ << TErrorAttribute("server_grid_step", *readGridStep)
+ << TErrorAttribute("grid_step", gridStep);
+ }
+
+ if (readGridStep->GetValue() % gridStep.GetValue() != 0) {
+ THROW_ERROR_EXCEPTION("Server grid step must be multiple of client grid step")
+ << TErrorAttribute("server_grid_step", *readGridStep)
+ << TErrorAttribute("grid_step", gridStep);
+ }
+
+ if (*readGridStep > *period) {
+ THROW_ERROR_EXCEPTION("Server grid step cannot be greater than fetch period")
+ << TErrorAttribute("server_grid_step", *readGridStep)
+ << TErrorAttribute("period", *period);
+ }
+
+ if (period->GetValue() % readGridStep->GetValue() != 0) {
+ THROW_ERROR_EXCEPTION("Server grid step must be multiple of fetch period")
+ << TErrorAttribute("server_grid_step", *readGridStep)
+ << TErrorAttribute("period", *period);
+ }
+ }
+}
+
+IYPathServicePtr TSolomonExporter::GetSensorService()
+{
+ return CreateSensorService(Config_, Registry_, MakeStrong(this));
+}
+
+TErrorOr<TReadWindow> TSolomonExporter::SelectReadWindow(
+ TInstant now,
+ TDuration period,
+ std::optional<TDuration> readGridStep,
+ TDuration gridStep)
+{
+ TReadWindow readWindow;
+
+ int gridSubsample = 1;
+ if (readGridStep) {
+ gridSubsample = *readGridStep / gridStep;
+ }
+
+ int gridFactor = gridStep / Config_->GridStep;
+
+ for (auto [iteration, time] : Window_) {
+ if (iteration % gridFactor != 0) {
+ continue;
+ }
+
+ int index = Registry_->IndexOf(iteration / gridFactor);
+ if (time >= now - period && time < now) {
+ if (readWindow.empty() ||
+ readWindow.back().first.size() >= static_cast<size_t>(gridSubsample)
+ ) {
+ readWindow.emplace_back(std::vector<int>{index}, time);
+ } else {
+ readWindow.back().first.push_back(index);
+ readWindow.back().second = time;
+ }
+ }
+ }
+
+ auto readGrid = gridStep;
+ if (readGridStep) {
+ readGrid = *readGridStep;
+ }
+
+ if (readWindow.size() != period / readGrid ||
+ readWindow.empty() ||
+ readWindow.back().first.size() != static_cast<size_t>(gridSubsample))
+ {
+ return TError("Read query is outside of window")
+ << TErrorAttribute("now", now)
+ << TErrorAttribute("period", period)
+ << TErrorAttribute("grid", readGridStep)
+ << TErrorAttribute("window_first", Window_.front().second)
+ << TErrorAttribute("window_last", Window_.back().second);
+ }
+
+ return readWindow;
+}
+
+void TSolomonExporter::CleanResponseCache()
+{
+ auto guard = Guard(CacheLock_);
+
+ std::vector<TCacheKey> toRemove;
+ for (const auto& [key, response] : ResponseCache_) {
+ if (key.Now < TInstant::Now() - Config_->ResponseCacheTtl) {
+ toRemove.push_back(key);
+ }
+ }
+
+ for (const auto& removedKey : toRemove) {
+ ResponseCache_.erase(removedKey);
+ }
+}
+
+bool TSolomonExporter::FilterDefaultGrid(const TString& sensorName)
+{
+ auto shard = Config_->MatchShard(sensorName);
+ if (shard && shard->GridStep) {
+ return Config_->GridStep == *shard->GridStep;
+ }
+
+ return true;
+}
+
+TSharedRef TSolomonExporter::DumpSensors()
+{
+ auto guard = WaitFor(TAsyncLockWriterGuard::Acquire(&Lock_))
+ .ValueOrThrow();
+
+ Registry_->ProcessRegistrations();
+ Registry_->Collect(OffloadThreadPool_->GetInvoker());
+
+ return SerializeProtoToRef(Registry_->DumpSensors());
+}
+
+void TSolomonExporter::AttachRemoteProcess(TCallback<TFuture<TSharedRef>()> dumpSensors)
+{
+ auto guard = Guard(RemoteProcessLock_);
+ RemoteProcessList_.insert(New<TRemoteProcess>(dumpSensors, Registry_.Get()));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSolomonExporter::TCacheKey::operator size_t() const
+{
+ size_t hash = 0;
+ HashCombine(hash, Shard);
+ HashCombine(hash, static_cast<int>(Format));
+ HashCombine(hash, static_cast<int>(Compression));
+ HashCombine(hash, Now);
+ HashCombine(hash, Period);
+ HashCombine(hash, Grid);
+ return hash;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/exporter.h b/yt/yt/library/profiling/solomon/exporter.h
new file mode 100644
index 0000000000..a734748334
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/exporter.h
@@ -0,0 +1,219 @@
+#pragma once
+
+#include "public.h"
+#include "registry.h"
+#include "remote.h"
+
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/async_rw_lock.h>
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/http/public.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+#include <yt/yt/core/ytree/ypath_detail.h>
+
+#include <library/cpp/monlib/encode/format.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/producer.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TShardConfig
+ : public NYTree::TYsonStruct
+{
+ std::vector<TString> Filter;
+
+ std::optional<TDuration> GridStep;
+
+ REGISTER_YSON_STRUCT(TShardConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TShardConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSolomonExporterConfig
+ : public NYTree::TYsonStruct
+{
+ TDuration GridStep;
+
+ TDuration LingerTimeout;
+
+ int WindowSize;
+
+ int ThreadPoolSize;
+
+ bool ConvertCountersToRateForSolomon;
+ bool RenameConvertedCounters;
+
+ bool ExportSummary;
+ bool ExportSummaryAsMax;
+ bool ExportSummaryAsAvg;
+
+ bool MarkAggregates;
+
+ bool StripSensorsNamePrefix;
+
+ bool EnableCoreProfilingCompatibility;
+
+ bool EnableSelfProfiling;
+
+ bool ReportBuildInfo;
+
+ bool ReportKernelVersion;
+
+ bool ReportRestart;
+
+ TDuration ResponseCacheTtl;
+
+ TDuration ReadDelay;
+
+ std::optional<TString> Host;
+
+ THashMap<TString, TString> InstanceTags;
+
+ THashMap<TString, TShardConfigPtr> Shards;
+
+ TDuration UpdateSensorServiceTreePeriod;
+
+ TShardConfigPtr MatchShard(const TString& sensorName);
+
+ REGISTER_YSON_STRUCT(TSolomonExporterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSolomonExporterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSolomonExporter
+ : public TRefCounted
+{
+public:
+ explicit TSolomonExporter(
+ TSolomonExporterConfigPtr config,
+ TSolomonRegistryPtr registry = nullptr);
+
+ void Register(const TString& prefix, const NYT::NHttp::IServerPtr& server);
+ void Register(const TString& prefix, const NYT::NHttp::IRequestPathMatcherPtr& handlers);
+
+ //! Attempts to read registered sensors in JSON format.
+ //! Returns null if exporter is not ready.
+ std::optional<TString> ReadJson(const TReadOptions& options = {}, std::optional<TString> shard = {});
+
+ std::optional<TString> ReadSpack(const TReadOptions& options = {}, std::optional<TString> shard = {});
+
+ bool ReadSensors(
+ ::NMonitoring::IMetricEncoderPtr encoder,
+ const TReadOptions& options,
+ std::optional<TString> shard);
+
+ void AttachRemoteProcess(TCallback<TFuture<TSharedRef>()> dumpSensors);
+
+ TSharedRef DumpSensors();
+
+ // There must be at most 1 running exporter per registry.
+ void Start();
+ void Stop();
+
+ NYTree::IYPathServicePtr GetSensorService();
+
+private:
+ const TSolomonExporterConfigPtr Config_;
+ const TSolomonRegistryPtr Registry_;
+
+ const NConcurrency::TActionQueuePtr ControlQueue_;
+ const NConcurrency::IThreadPoolPtr OffloadThreadPool_;
+
+ TFuture<void> CollectorFuture_;
+
+ NConcurrency::TAsyncReaderWriterLock Lock_;
+ std::vector<std::pair<i64, TInstant>> Window_;
+
+ TInstant StartTime_ = TInstant::Now();
+
+ TSpinLock StatusLock_;
+ std::optional<TInstant> LastFetch_;
+ THashMap<TString, std::optional<TInstant>> LastShardFetch_;
+
+ struct TCacheKey
+ {
+ std::optional<TString> Shard;
+ ::NMonitoring::EFormat Format;
+ ::NMonitoring::ECompression Compression;
+
+ TInstant Now;
+ TDuration Period;
+ std::optional<TDuration> Grid;
+
+ bool operator == (const TCacheKey& other) const = default;
+
+ operator size_t () const;
+ };
+
+ TSpinLock CacheLock_;
+ THashMap<TCacheKey, TFuture<TSharedRef>> ResponseCache_;
+
+ TEventTimer CollectionStartDelay_;
+ TCounter WindowErrors_;
+ TCounter ReadDelays_;
+ TCounter ResponseCacheHit_, ResponseCacheMiss_;
+
+ struct TRemoteProcess final
+ {
+ TRemoteProcess(TCallback<TFuture<TSharedRef>()> dumpSensors, TSolomonRegistry* registry)
+ : DumpSensors(dumpSensors)
+ , Registry(registry)
+ { }
+
+ TCallback<TFuture<TSharedRef>()> DumpSensors;
+ TRemoteRegistry Registry;
+ };
+
+ TSpinLock RemoteProcessLock_;
+ THashSet<TIntrusivePtr<TRemoteProcess>> RemoteProcessList_;
+
+ void DoCollect();
+ void TransferSensors();
+
+ void HandleIndex(const TString& prefix, const NHttp::IRequestPtr& req, const NHttp::IResponseWriterPtr& rsp);
+ void HandleStatus(const NHttp::IRequestPtr& req, const NHttp::IResponseWriterPtr& rsp);
+
+ void HandleDebugSensors(const NHttp::IRequestPtr& req, const NHttp::IResponseWriterPtr& rsp);
+ void HandleDebugTags(const NHttp::IRequestPtr& req, const NHttp::IResponseWriterPtr& rsp);
+
+ void HandleShard(
+ const std::optional<TString>& name,
+ const NHttp::IRequestPtr& req,
+ const NHttp::IResponseWriterPtr& rsp);
+
+ void DoHandleShard(
+ const std::optional<TString>& name,
+ const NHttp::IRequestPtr& req,
+ const NHttp::IResponseWriterPtr& rsp);
+
+ void ValidatePeriodAndGrid(std::optional<TDuration> period, std::optional<TDuration> readGridStep, TDuration gridStep);
+
+ TErrorOr<TReadWindow> SelectReadWindow(TInstant now, TDuration period, std::optional<TDuration> readGridStep, TDuration gridStep);
+
+ void CleanResponseCache();
+
+ bool FilterDefaultGrid(const TString& sensorName);
+
+ // For locking.
+ friend class TSensorService;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSolomonExporter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/percpu.cpp b/yt/yt/library/profiling/solomon/percpu.cpp
new file mode 100644
index 0000000000..0f146f0921
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/percpu.cpp
@@ -0,0 +1,133 @@
+#include "percpu.h"
+#include "yt/yt/library/profiling/summary.h"
+
+#include <yt/yt/core/profiling/tscp.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPerCpuCounter::Increment(i64 delta)
+{
+ auto tscp = TTscp::Get();
+ Shards_[tscp.ProcessorId].Value.fetch_add(delta, std::memory_order::relaxed);
+}
+
+i64 TPerCpuCounter::GetValue()
+{
+ i64 total = 0;
+ for (const auto& shard : Shards_) {
+ total += shard.Value.load();
+ }
+ return total;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPerCpuTimeCounter::Add(TDuration delta)
+{
+ auto tscp = TTscp::Get();
+ Shards_[tscp.ProcessorId].Value.fetch_add(delta.GetValue(), std::memory_order::relaxed);
+}
+
+TDuration TPerCpuTimeCounter::GetValue()
+{
+ TDuration total = TDuration::Zero();
+ for (const auto& shard : Shards_) {
+ total += TDuration::FromValue(shard.Value.load());
+ }
+ return total;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+__int128 TPerCpuGauge::TWrite::Pack()
+{
+ static_assert(sizeof(TWrite) == 16);
+
+ __int128 i;
+ memcpy(&i, this, 16);
+ return i;
+}
+
+TPerCpuGauge::TWrite TPerCpuGauge::TWrite::Unpack(__int128 i)
+{
+ TWrite w;
+ memcpy(&w, &i, 16);
+ return w;
+}
+
+void TPerCpuGauge::Update(double value)
+{
+ auto tscp = TTscp::Get();
+
+ TWrite write{value, tscp.Instant};
+#ifdef __clang__
+ Shards_[tscp.ProcessorId].Value.store(write.Pack(), std::memory_order::relaxed);
+#else
+ auto guard = Guard(Shards_[tscp.ProcessorId].Lock);
+ Shards_[tscp.ProcessorId].Value = write;
+#endif
+}
+
+double TPerCpuGauge::GetValue()
+{
+ double lastValue = 0.0;
+ TCpuInstant maxTimestamp = 0;
+
+ for (const auto& shard : Shards_) {
+#ifdef __clang__
+ auto write = TWrite::Unpack(shard.Value.load());
+#else
+ auto guard = Guard(shard.Lock);
+ auto write = shard.Value;
+#endif
+
+ if (write.Timestamp > maxTimestamp) {
+ maxTimestamp = write.Timestamp;
+ lastValue = write.Value;
+ }
+ }
+
+ return lastValue;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TPerCpuSummary<T>::Record(T value)
+{
+ auto tscp = TTscp::Get();
+ auto guard = Guard(Shards_[tscp.ProcessorId].Lock);
+ Shards_[tscp.ProcessorId].Value.Record(value);
+}
+
+template <class T>
+TSummarySnapshot<T> TPerCpuSummary<T>::GetSummary()
+{
+ TSummarySnapshot<T> value;
+ for (const auto& shard : Shards_) {
+ auto guard = Guard(shard.Lock);
+ value += shard.Value;
+ }
+ return value;
+}
+
+template <class T>
+TSummarySnapshot<T> TPerCpuSummary<T>::GetSummaryAndReset()
+{
+ TSummarySnapshot<T> value;
+ for (auto& shard : Shards_) {
+ auto guard = Guard(shard.Lock);
+ value += shard.Value;
+ shard.Value = {};
+ }
+ return value;
+}
+
+template class TPerCpuSummary<double>;
+template class TPerCpuSummary<TDuration>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/percpu.h b/yt/yt/library/profiling/solomon/percpu.h
new file mode 100644
index 0000000000..e6e3be2a05
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/percpu.h
@@ -0,0 +1,120 @@
+#pragma once
+
+#include <yt/yt/library/profiling/impl.h>
+#include <yt/yt/library/profiling/summary.h>
+
+#include <yt/yt/core/profiling/tscp.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPerCpuCounter
+ : public ICounterImpl
+{
+public:
+ void Increment(i64 delta) override;
+
+ i64 GetValue() override;
+
+private:
+ struct alignas(CacheLineSize) TShard
+ {
+ std::atomic<i64> Value = 0;
+ };
+
+ std::array<TShard, TTscp::MaxProcessorId> Shards_;
+};
+
+static_assert(sizeof(TPerCpuCounter) == 64 + 64 * 64);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPerCpuTimeCounter
+ : public ITimeCounterImpl
+{
+public:
+ void Add(TDuration delta) override;
+
+ TDuration GetValue() override;
+
+private:
+ struct alignas(CacheLineSize) TShard
+ {
+ std::atomic<TDuration::TValue> Value = 0;
+ };
+
+ std::array<TShard, TTscp::MaxProcessorId> Shards_;
+};
+
+static_assert(sizeof(TPerCpuCounter) == 64 + 64 * 64);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPerCpuGauge
+ : public IGaugeImpl
+{
+public:
+ void Update(double value) override;
+
+ double GetValue() override;
+
+private:
+ struct TWrite
+ {
+ double Value;
+ TCpuInstant Timestamp;
+
+ __int128 Pack();
+ static TWrite Unpack(__int128 i);
+ };
+
+ struct alignas(CacheLineSize) TShard
+ {
+#ifdef __clang__
+ std::atomic<__int128> Value = {};
+#else
+ TSpinLock Lock;
+ TWrite Value;
+#endif
+ };
+
+#ifdef __clang__
+ static_assert(std::atomic<TWrite>::is_always_lock_free);
+#endif
+
+ std::array<TShard, TTscp::MaxProcessorId> Shards_;
+};
+
+static_assert(sizeof(TPerCpuCounter) == 64 + 64 * 64);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TPerCpuSummary
+ : public ISummaryImplBase<T>
+{
+public:
+ void Record(T value) override;
+
+ TSummarySnapshot<T> GetSummary() override;
+ TSummarySnapshot<T> GetSummaryAndReset() override;
+
+private:
+ struct alignas(CacheLineSize) TShard
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock);
+ TSummarySnapshot<T> Value;
+ };
+
+ std::array<TShard, TTscp::MaxProcessorId> Shards_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TPerCpuSummary<double>)
+DEFINE_REFCOUNTED_TYPE(TPerCpuSummary<TDuration>)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/private.h b/yt/yt/library/profiling/solomon/private.h
new file mode 100644
index 0000000000..f011a0e471
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/private.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger SolomonLogger("Solomon");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/producer.cpp b/yt/yt/library/profiling/solomon/producer.cpp
new file mode 100644
index 0000000000..e54ed9e6ce
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/producer.cpp
@@ -0,0 +1,216 @@
+#include "producer.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/library/profiling/producer.h>
+
+namespace NYT::NProfiling {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_REFCOUNTED_TYPE(TProducerState)
+DEFINE_REFCOUNTED_TYPE(TProducerCounters)
+
+////////////////////////////////////////////////////////////////////////////////
+
+const static auto& Logger = SolomonLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TProducerCounters::ClearOutdated(i64 lastIteration)
+{
+ std::vector<TString> countersToRemove;
+ for (const auto& [name, counter] : Counters) {
+ if (std::get<1>(counter) != lastIteration) {
+ countersToRemove.push_back(name);
+ }
+ }
+ for (const auto& name : countersToRemove) {
+ Counters.erase(name);
+ }
+
+ std::vector<TString> gaugesToRemove;
+ for (const auto& [name, gauge] : Gauges) {
+ if (gauge.second != lastIteration) {
+ gaugesToRemove.push_back(name);
+ }
+ }
+ for (const auto& name : gaugesToRemove) {
+ Gauges.erase(name);
+ }
+
+ std::vector<TTag> tagsToRemove;
+ for (auto& [tag, set] : Tags) {
+ set.first->ClearOutdated(lastIteration);
+
+ if (set.second != lastIteration || set.first->IsEmpty()) {
+ tagsToRemove.push_back(tag);
+ }
+ }
+ for (const auto& tag : tagsToRemove) {
+ Tags.erase(tag);
+ }
+}
+
+bool TProducerCounters::IsEmpty() const
+{
+ return Counters.empty() && Gauges.empty() && Tags.empty();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCounterWriter::TCounterWriter(
+ IRegistryImplPtr registry,
+ TProducerCountersPtr counters,
+ i64 iteration)
+ : Registry_(std::move(registry))
+ , Counters_{{std::move(counters)}}
+ , Iteration_{iteration}
+{ }
+
+void TCounterWriter::PushTag(TTag tag)
+{
+ auto& [nested, iteration] = Counters_.back()->Tags[tag];
+ iteration = Iteration_;
+
+ if (!nested) {
+ nested = New<TProducerCounters>();
+ nested->Prefix = Counters_.back()->Prefix;
+ nested->ProducerTags = Counters_.back()->ProducerTags;
+ nested->Options = Counters_.back()->Options;
+ nested->ProducerTags.AddTag(std::move(tag));
+ }
+
+ Counters_.push_back(nested);
+}
+
+void TCounterWriter::PopTag()
+{
+ Counters_.pop_back();
+}
+
+void TCounterWriter::AddGauge(const TString& name, double value)
+{
+ auto& [gauge, iteration] = Counters_.back()->Gauges[name];
+ iteration = Iteration_;
+
+ if (!gauge) {
+ TProfiler profiler{
+ Counters_.back()->Prefix,
+ "",
+ Counters_.back()->ProducerTags,
+ Registry_,
+ Counters_.back()->Options,
+ };
+
+ gauge = profiler.Gauge(name);
+ }
+
+ gauge.Update(value);
+}
+
+void TCounterWriter::AddCounter(const TString& name, i64 value)
+{
+ auto& [counter, iteration, lastValue] = Counters_.back()->Counters[name];
+ iteration = Iteration_;
+
+ if (!counter) {
+ TProfiler profiler{
+ Counters_.back()->Prefix,
+ "",
+ Counters_.back()->ProducerTags,
+ Registry_,
+ Counters_.back()->Options,
+ };
+
+ counter = profiler.Counter(name);
+ }
+
+ if (value >= lastValue) {
+ auto delta = value - lastValue;
+ counter.Increment(delta);
+ lastValue = value;
+ } else {
+ // Some producers use counter incorrectly.
+ lastValue = value;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TProducerSet::AddProducer(TProducerStatePtr state)
+{
+ Producers_.insert(std::move(state));
+}
+
+void TProducerSet::Collect(IRegistryImplPtr profiler, IInvokerPtr invoker)
+{
+ std::vector<TFuture<void>> offloadFutures;
+ std::deque<TProducerStatePtr> toRemove;
+ for (const auto& producer : Producers_) {
+ auto owner = producer->Producer.Lock();
+ if (!owner) {
+ toRemove.push_back(producer);
+ continue;
+ }
+
+ auto future = BIND([profiler, owner, producer, collectDuration = ProducerCollectDuration_] () {
+ auto startTime = TInstant::Now();
+ auto reportTime = Finally([&] {
+ collectDuration.Record(TInstant::Now() - startTime);
+ });
+
+ try {
+ auto buffer = owner->GetBuffer();
+ if (buffer) {
+ auto lastBuffer = producer->LastBuffer.Lock();
+ if (lastBuffer == buffer) {
+ return;
+ }
+
+ TCounterWriter writer(profiler, producer->Counters, ++producer->LastUpdateIteration);
+ buffer->WriteTo(&writer);
+ producer->LastBuffer = buffer;
+ if (producer->Counters->Options.ProducerRemoveSupport) {
+ producer->Counters->ClearOutdated(producer->LastUpdateIteration);
+ }
+ } else {
+ producer->Counters->Counters.clear();
+ producer->Counters->Gauges.clear();
+ producer->Counters->Tags.clear();
+ }
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Producer read failed");
+ return;
+ }
+ })
+ .AsyncVia(invoker)
+ .Run();
+
+ offloadFutures.push_back(future);
+ }
+
+ // Use blocking Get(), because we want to lock current thread while data structure is updating.
+ for (const auto& future : offloadFutures) {
+ future.Get();
+ }
+
+ for (const auto& producer : toRemove) {
+ Producers_.erase(producer);
+ }
+}
+
+void TProducerSet::Profile(const TProfiler& profiler)
+{
+ SelfProfiler_ = profiler;
+ ProducerCollectDuration_ = profiler.Timer("/producer_collect_duration");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/producer.h b/yt/yt/library/profiling/solomon/producer.h
new file mode 100644
index 0000000000..188309d68e
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/producer.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include "cube.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/library/profiling/impl.h>
+#include <yt/yt/library/profiling/producer.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TProducerCounters)
+
+struct TProducerCounters final
+{
+ TString Prefix;
+ TTagSet ProducerTags;
+ TSensorOptions Options;
+
+ THashMap<TString, std::pair<TGauge, i64>> Gauges;
+ THashMap<TString, std::tuple<TCounter, i64, i64>> Counters;
+ THashMap<TTag, std::pair<TProducerCountersPtr, i64>> Tags;
+
+ void ClearOutdated(i64 lastIteration);
+ bool IsEmpty() const;
+};
+
+class TCounterWriter final
+ : public ISensorWriter
+{
+public:
+ TCounterWriter(
+ IRegistryImplPtr registry,
+ TProducerCountersPtr counters,
+ i64 iteration);
+
+ void PushTag(TTag tag) override;
+ void PopTag() override;
+ void AddGauge(const TString& name, double value) override;
+ void AddCounter(const TString& name, i64 value) override;
+
+private:
+ IRegistryImplPtr Registry_;
+ std::vector<TProducerCountersPtr> Counters_;
+ i64 Iteration_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TProducerState)
+
+struct TProducerState final
+{
+ TProducerState(
+ const TString& prefix,
+ const TTagSet& tags,
+ TSensorOptions options,
+ TWeakPtr<ISensorProducer> producer)
+ : Producer(std::move(producer))
+ , Counters(New<TProducerCounters>())
+ {
+ Counters->Prefix = prefix;
+ Counters->ProducerTags = tags;
+ Counters->Options = options;
+ }
+
+ const TWeakPtr<ISensorProducer> Producer;
+ TWeakPtr<TSensorBuffer> LastBuffer;
+
+ i64 LastUpdateIteration = 0;
+ TProducerCountersPtr Counters;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProducerSet
+{
+public:
+ void AddProducer(TProducerStatePtr state);
+
+ void Collect(IRegistryImplPtr profiler, IInvokerPtr invoker);
+
+ void Profile(const TProfiler& profiler);
+
+private:
+ THashSet<TProducerStatePtr> Producers_;
+
+ TProfiler SelfProfiler_;
+ TEventTimer ProducerCollectDuration_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/public.h b/yt/yt/library/profiling/solomon/public.h
new file mode 100644
index 0000000000..6942e8dba7
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/public.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <yt/yt/core/misc/ref_counted.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TShardConfig)
+DECLARE_REFCOUNTED_STRUCT(TSolomonExporterConfig)
+DECLARE_REFCOUNTED_CLASS(TSolomonExporter)
+DECLARE_REFCOUNTED_CLASS(TSolomonRegistry)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/registry.cpp b/yt/yt/library/profiling/solomon/registry.cpp
new file mode 100644
index 0000000000..bd24c87b04
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/registry.cpp
@@ -0,0 +1,544 @@
+#include "registry.h"
+
+#include "sensor.h"
+#include "percpu.h"
+
+#include <type_traits>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <yt/yt/library/profiling/impl.h>
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NProfiling {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSolomonRegistry::TSolomonRegistry()
+{ }
+
+template <class TBase, class TSimple, class TPerCpu, class TFn>
+TIntrusivePtr<TBase> SelectImpl(bool hot, const TFn& fn)
+{
+ if (!hot) {
+ auto counter = New<TSimple>();
+ fn(counter);
+ return counter;
+ } else {
+ auto counter = New<TPerCpu>();
+ fn(counter);
+ return counter;
+ }
+}
+
+ICounterImplPtr TSolomonRegistry::RegisterCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ return SelectImpl<ICounterImpl, TSimpleCounter, TPerCpuCounter>(options.Hot, [&, this] (const auto& counter) {
+ DoRegister([this, name, tags, options, counter] () {
+ auto reader = [ptr = counter.Get()] {
+ return ptr->GetValue();
+ };
+
+ auto set = FindSet(name, options);
+ set->AddCounter(New<TCounterState>(counter, reader, Tags_.Encode(tags), tags));
+ });
+ });
+}
+
+ITimeCounterImplPtr TSolomonRegistry::RegisterTimeCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ return SelectImpl<ITimeCounterImpl, TSimpleTimeCounter, TPerCpuTimeCounter>(
+ options.Hot,
+ [&, this] (const auto& counter) {
+ DoRegister([this, name, tags, options, counter] () {
+ auto set = FindSet(name, options);
+ set->AddTimeCounter(New<TTimeCounterState>(counter, Tags_.Encode(tags), tags));
+ });
+ });
+}
+
+IGaugeImplPtr TSolomonRegistry::RegisterGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ return SelectImpl<IGaugeImpl, TSimpleGauge, TPerCpuGauge>(options.Hot, [&, this] (const auto& gauge) {
+ if (options.DisableDefault) {
+ gauge->Update(std::numeric_limits<double>::quiet_NaN());
+ }
+
+ DoRegister([this, name, tags, options, gauge] () {
+ auto reader = [ptr = gauge.Get()] {
+ return ptr->GetValue();
+ };
+
+ auto set = FindSet(name, options);
+ set->AddGauge(New<TGaugeState>(gauge, reader, Tags_.Encode(tags), tags));
+ });
+ });
+}
+
+ITimeGaugeImplPtr TSolomonRegistry::RegisterTimeGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ auto gauge = New<TSimpleTimeGauge>();
+
+ DoRegister([this, name, tags, options, gauge] () {
+ auto reader = [ptr = gauge.Get()] {
+ return ptr->GetValue().SecondsFloat();
+ };
+
+ auto set = FindSet(name, options);
+ set->AddGauge(New<TGaugeState>(gauge, reader, Tags_.Encode(tags), tags));
+ });
+
+ return gauge;
+}
+
+ISummaryImplPtr TSolomonRegistry::RegisterSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ return SelectImpl<ISummaryImpl, TSimpleSummary<double>, TPerCpuSummary<double>>(options.Hot, [&, this] (const auto& summary) {
+ DoRegister([this, name, tags, options, summary] () {
+ auto set = FindSet(name, options);
+ set->AddSummary(New<TSummaryState>(summary, Tags_.Encode(tags), tags));
+ });
+ });
+}
+
+IGaugeImplPtr TSolomonRegistry::RegisterGaugeSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ auto gauge = New<TSimpleGauge>();
+ DoRegister([this, name, tags, options, gauge] () {
+ auto set = FindSet(name, options);
+ set->AddSummary(New<TSummaryState>(gauge, Tags_.Encode(tags), tags));
+ });
+
+ return gauge;
+}
+
+ITimeGaugeImplPtr TSolomonRegistry::RegisterTimeGaugeSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ auto gauge = New<TSimpleTimeGauge>();
+ DoRegister([this, name, tags, options, gauge] () {
+ auto set = FindSet(name, options);
+ set->AddTimerSummary(New<TTimerSummaryState>(gauge, Tags_.Encode(tags), tags));
+ });
+
+ return gauge;
+}
+
+ITimerImplPtr TSolomonRegistry::RegisterTimerSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ return SelectImpl<ITimerImpl, TSimpleSummary<TDuration>, TPerCpuSummary<TDuration>>(
+ options.Hot,
+ [&, this] (const auto& timer) {
+ DoRegister([this, name, tags, options, timer] () {
+ auto set = FindSet(name, options);
+ set->AddTimerSummary(New<TTimerSummaryState>(timer, Tags_.Encode(tags), tags));
+ });
+ });
+}
+
+ITimerImplPtr TSolomonRegistry::RegisterTimeHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ auto hist = New<THistogram>(options);
+ DoRegister([this, name, tags, options, hist] () {
+ auto set = FindSet(name, options);
+ set->AddTimeHistogram(New<THistogramState>(hist, Tags_.Encode(tags), tags));
+ });
+ return hist;
+}
+
+IHistogramImplPtr TSolomonRegistry::RegisterGaugeHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ auto hist = New<THistogram>(options);
+ DoRegister([this, name, tags, options, hist] () {
+ auto set = FindSet(name, options);
+ set->AddGaugeHistogram(New<THistogramState>(hist, Tags_.Encode(tags), tags));
+ });
+ return hist;
+}
+
+IHistogramImplPtr TSolomonRegistry::RegisterRateHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options)
+{
+ auto hist = New<THistogram>(options);
+ DoRegister([this, name, tags, options, hist] () {
+ auto set = FindSet(name, options);
+ set->AddRateHistogram(New<THistogramState>(hist, Tags_.Encode(tags), tags));
+ });
+ return hist;
+}
+
+void TSolomonRegistry::RegisterFuncCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const TRefCountedPtr& owner,
+ std::function<i64()> reader)
+{
+ DoRegister([this, name, tags, options, owner, reader] () {
+ auto set = FindSet(name, options);
+ set->AddCounter(New<TCounterState>(owner, reader, Tags_.Encode(tags), tags));
+ });
+}
+
+void TSolomonRegistry::RegisterFuncGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const TRefCountedPtr& owner,
+ std::function<double()> reader)
+{
+ DoRegister([this, name, tags, options, owner, reader] () {
+ auto set = FindSet(name, options);
+ set->AddGauge(New<TGaugeState>(owner, reader, Tags_.Encode(tags), tags));
+ });
+}
+
+void TSolomonRegistry::RegisterProducer(
+ const TString& prefix,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const ISensorProducerPtr& producer)
+{
+ DoRegister([this, prefix, tags, options, producer] () {
+ Producers_.AddProducer(New<TProducerState>(prefix, tags, options, producer));
+ });
+}
+
+void TSolomonRegistry::RenameDynamicTag(
+ const TDynamicTagPtr& tag,
+ const TString& name,
+ const TString& value)
+{
+ DoRegister([this, tag, name, value] {
+ auto tagId = Tags_.Encode(TTag{name, value});
+
+ for (auto& [name, sensorSet] : Sensors_) {
+ sensorSet.RenameDynamicTag(tag, tagId);
+ }
+ });
+}
+
+TSolomonRegistryPtr TSolomonRegistry::Get()
+{
+ return LeakyRefCountedSingleton<TSolomonRegistry>();
+}
+
+i64 TSolomonRegistry::GetNextIteration() const
+{
+ return Iteration_;
+}
+
+void TSolomonRegistry::SetGridFactor(std::function<int(const TString&)> gridFactor)
+{
+ GridFactor_ = gridFactor;
+}
+
+void TSolomonRegistry::SetWindowSize(int windowSize)
+{
+ if (WindowSize_) {
+ THROW_ERROR_EXCEPTION("Window size is already set");
+ }
+
+ WindowSize_ = windowSize;
+}
+
+int TSolomonRegistry::GetWindowSize() const
+{
+ if (!WindowSize_) {
+ THROW_ERROR_EXCEPTION("Window size is not configured");
+ }
+
+ return *WindowSize_;
+}
+
+int TSolomonRegistry::IndexOf(i64 iteration) const
+{
+ return iteration % GetWindowSize();
+}
+
+void TSolomonRegistry::Profile(const TProfiler& profiler)
+{
+ SelfProfiler_ = profiler.WithPrefix("/solomon_registry");
+
+ Producers_.Profile(SelfProfiler_);
+
+ SensorCollectDuration_ = SelfProfiler_.Timer("/sensor_collect_duration");
+ ReadDuration_ = SelfProfiler_.Timer("/read_duration");
+ SensorCount_ = SelfProfiler_.Gauge("/sensor_count");
+ ProjectionCount_ = SelfProfiler_.Gauge("/projection_count");
+ TagCount_ = SelfProfiler_.Gauge("/tag_count");
+ RegistrationCount_ = SelfProfiler_.Counter("/registration_count");
+}
+
+const TProfiler& TSolomonRegistry::GetSelfProfiler() const
+{
+ return SelfProfiler_;
+}
+
+template <class TFn>
+void TSolomonRegistry::DoRegister(TFn fn)
+{
+ if (Disabled_) {
+ return;
+ }
+
+ RegistrationQueue_.Enqueue(std::move(fn));
+}
+
+void TSolomonRegistry::SetDynamicTags(std::vector<TTag> dynamicTags)
+{
+ auto guard = Guard(DynamicTagsLock_);
+ std::swap(DynamicTags_, dynamicTags);
+}
+
+std::vector<TTag> TSolomonRegistry::GetDynamicTags()
+{
+ auto guard = Guard(DynamicTagsLock_);
+ return DynamicTags_;
+}
+
+void TSolomonRegistry::Disable()
+{
+ Disabled_ = true;
+ RegistrationQueue_.DequeueAll();
+}
+
+void TSolomonRegistry::ProcessRegistrations()
+{
+ GetWindowSize();
+
+ RegistrationQueue_.DequeueAll(true, [this] (const std::function<void()>& fn) {
+ RegistrationCount_.Increment();
+
+ fn();
+
+ TagCount_.Update(Tags_.GetSize());
+ });
+}
+
+void TSolomonRegistry::Collect(IInvokerPtr offloadInvoker)
+{
+ Producers_.Collect(MakeStrong(this), offloadInvoker);
+ ProcessRegistrations();
+
+ auto projectionCount = std::make_shared<std::atomic<int>>(0);
+
+ std::vector<TFuture<void>> offloadFutures;
+ for (auto& [name, set] : Sensors_) {
+ if (Iteration_ % set.GetGridFactor() != 0) {
+ continue;
+ }
+
+ auto future = BIND([sensorSet = &set, projectionCount, collectDuration = SensorCollectDuration_] {
+ auto start = TInstant::Now();
+ *projectionCount += sensorSet->Collect();
+ collectDuration.Record(TInstant::Now() - start);
+ })
+ .AsyncVia(offloadInvoker)
+ .Run();
+
+ offloadFutures.push_back(future);
+ }
+
+ // Use blocking Get(), because we want to lock current thread while data structure is updating.
+ for (const auto& future : offloadFutures) {
+ future.Get();
+ }
+
+ ProjectionCount_.Update(*projectionCount);
+ Iteration_++;
+}
+
+void TSolomonRegistry::ReadSensors(
+ const TReadOptions& options,
+ ::NMonitoring::IMetricConsumer* consumer) const
+{
+ auto readOptions = options;
+ {
+ auto guard = Guard(DynamicTagsLock_);
+ readOptions.InstanceTags.insert(
+ readOptions.InstanceTags.end(),
+ DynamicTags_.begin(),
+ DynamicTags_.end());
+ }
+
+ TTagWriter tagWriter(Tags_, consumer);
+ for (const auto& [name, set] : Sensors_) {
+ if (readOptions.SensorFilter && !readOptions.SensorFilter(name)) {
+ continue;
+ }
+
+ auto start = TInstant::Now();
+ set.ReadSensors(name, readOptions, &tagWriter, consumer);
+ ReadDuration_.Record(TInstant::Now() - start);
+ }
+}
+
+void TSolomonRegistry::ReadRecentSensorValues(
+ const TString& name,
+ const TTagList& tags,
+ const TReadOptions& options,
+ TFluentAny fluent) const
+{
+ if (Iteration_ == 0) {
+ THROW_ERROR_EXCEPTION(NYTree::EErrorCode::ResolveError,
+ "No sensors have been collected so far");
+ }
+
+ auto it = Sensors_.find(name);
+ if (it == Sensors_.end()) {
+ THROW_ERROR_EXCEPTION(NYTree::EErrorCode::ResolveError,
+ "No such sensor")
+ << TErrorAttribute("name", name);
+ }
+
+ const auto& sensorSet = it->second;
+ auto index = IndexOf((Iteration_ - 1) / sensorSet.GetGridFactor());
+
+ auto readOptions = options;
+ {
+ auto guard = Guard(DynamicTagsLock_);
+ readOptions.InstanceTags.insert(
+ readOptions.InstanceTags.end(),
+ DynamicTags_.begin(),
+ DynamicTags_.end());
+ }
+
+ auto encodedTagIds = Tags_.TryEncode(tags);
+ std::optional<TTagIdList> tagIds = TTagIdList{};
+ for (int i = 0; i < std::ssize(encodedTagIds); ++i) {
+ if (encodedTagIds[i]) {
+ tagIds->push_back(*encodedTagIds[i]);
+ continue;
+ }
+
+ auto tagIt = std::find(
+ readOptions.InstanceTags.begin(),
+ readOptions.InstanceTags.end(),
+ tags[i]);
+ if (tagIt == readOptions.InstanceTags.end()) {
+ tagIds.reset();
+ break;
+ }
+ }
+
+ int valuesRead = 0;
+ if (tagIds) {
+ std::sort(tagIds->begin(), tagIds->end());
+ valuesRead = sensorSet.ReadSensorValues(*tagIds, index, readOptions, Tags_, fluent);
+ }
+
+ if (!readOptions.ReadAllProjections) {
+ if (valuesRead == 0) {
+ THROW_ERROR_EXCEPTION(NYTree::EErrorCode::ResolveError,
+ "Projection not found for sensor")
+ << TErrorAttribute("name", name)
+ << TErrorAttribute("tags", tags);
+ } else if (valuesRead > 1) {
+ THROW_ERROR_EXCEPTION(NYTree::EErrorCode::ResolveError,
+ "More than one projection found for sensor")
+ << TErrorAttribute("name", name)
+ << TErrorAttribute("tags", tags)
+ << TErrorAttribute("values_read", valuesRead);
+ }
+ } else if (valuesRead == 0) {
+ fluent.BeginList().EndList();
+ }
+}
+
+std::vector<TSensorInfo> TSolomonRegistry::ListSensors() const
+{
+ std::vector<TSensorInfo> list;
+ for (const auto& [name, set] : Sensors_) {
+ list.push_back(TSensorInfo{name, set.GetObjectCount(), set.GetCubeSize(), set.GetError()});
+ }
+ return list;
+}
+
+const TTagRegistry& TSolomonRegistry::GetTags() const
+{
+ return Tags_;
+}
+
+TSensorSet* TSolomonRegistry::FindSet(const TString& name, const TSensorOptions& options)
+{
+ if (auto it = Sensors_.find(name); it != Sensors_.end()) {
+ it->second.ValidateOptions(options);
+ return &it->second;
+ } else {
+ int gridFactor = 1;
+ if (GridFactor_) {
+ gridFactor = GridFactor_(name);
+ }
+
+ it = Sensors_.emplace(name, TSensorSet{options, Iteration_ / gridFactor, GetWindowSize(), gridFactor}).first;
+ it->second.Profile(SelfProfiler_.WithTag("metric_name", name));
+ SensorCount_.Update(Sensors_.size());
+ return &it->second;
+ }
+}
+
+NProto::TSensorDump TSolomonRegistry::DumpSensors() {
+ NProto::TSensorDump dump;
+ Tags_.DumpTags(&dump);
+
+ for (const auto& [name, set] : Sensors_) {
+ if (!set.GetError().IsOK()) {
+ continue;
+ }
+
+ auto cube = dump.add_cubes();
+ cube->set_name(name);
+ set.DumpCube(cube);
+ }
+
+ return dump;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef _unix_
+// This function overrides weak symbol defined in impl.cpp
+IRegistryImplPtr GetGlobalRegistry()
+{
+ return TSolomonRegistry::Get();
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/registry.h b/yt/yt/library/profiling/solomon/registry.h
new file mode 100644
index 0000000000..0569f18cd9
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/registry.h
@@ -0,0 +1,189 @@
+#pragma once
+
+#include "public.h"
+#include "sensor_set.h"
+#include "producer.h"
+#include "tag_registry.h"
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/misc/mpsc_stack.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/impl.h>
+
+#include <yt/yt/library/profiling/solomon/sensor_dump.pb.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSensorInfo
+{
+ TString Name;
+ int ObjectCount;
+ int CubeSize;
+ TError Error;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSolomonRegistry
+ : public IRegistryImpl
+{
+public:
+ explicit TSolomonRegistry();
+
+ ICounterImplPtr RegisterCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ ITimeCounterImplPtr RegisterTimeCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ IGaugeImplPtr RegisterGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ ITimeGaugeImplPtr RegisterTimeGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ ISummaryImplPtr RegisterSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ IGaugeImplPtr RegisterGaugeSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ ITimeGaugeImplPtr RegisterTimeGaugeSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ ITimerImplPtr RegisterTimerSummary(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ ITimerImplPtr RegisterTimeHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ IHistogramImplPtr RegisterGaugeHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ IHistogramImplPtr RegisterRateHistogram(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options) override;
+
+ void RegisterFuncCounter(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const TRefCountedPtr& owner,
+ std::function<i64()> reader) override;
+
+ void RegisterFuncGauge(
+ const TString& name,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const TRefCountedPtr& owner,
+ std::function<double()> reader) override;
+
+ void RegisterProducer(
+ const TString& prefix,
+ const TTagSet& tags,
+ TSensorOptions options,
+ const ISensorProducerPtr& owner) override;
+
+ void RenameDynamicTag(
+ const TDynamicTagPtr& tag,
+ const TString& name,
+ const TString& value) override;
+
+ static TSolomonRegistryPtr Get();
+
+ void Disable();
+ void SetDynamicTags(std::vector<TTag> dynamicTags);
+ std::vector<TTag> GetDynamicTags();
+
+ void SetGridFactor(std::function<int(const TString&)> gridFactor);
+ void SetWindowSize(int windowSize);
+ void ProcessRegistrations();
+ void Collect(IInvokerPtr offloadInvoker = GetSyncInvoker());
+ void ReadSensors(
+ const TReadOptions& options,
+ ::NMonitoring::IMetricConsumer* consumer) const;
+
+ void ReadRecentSensorValues(
+ const TString& name,
+ const TTagList& tags,
+ const TReadOptions& options,
+ NYTree::TFluentAny fluent) const;
+
+ std::vector<TSensorInfo> ListSensors() const;
+
+ const TTagRegistry& GetTags() const;
+
+ i64 GetNextIteration() const;
+ int GetWindowSize() const;
+ int IndexOf(i64 iteration) const;
+
+ void Profile(const TProfiler& profiler);
+ const TProfiler& GetSelfProfiler() const;
+
+ NProto::TSensorDump DumpSensors();
+
+private:
+ i64 Iteration_ = 0;
+ std::optional<int> WindowSize_;
+ std::function<int(const TString&)> GridFactor_;
+ TProfiler SelfProfiler_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, DynamicTagsLock_);
+ std::vector<TTag> DynamicTags_;
+
+ std::atomic<bool> Disabled_ = false;
+ TMpscStack<std::function<void()>> RegistrationQueue_;
+
+ template <class TFn>
+ void DoRegister(TFn fn);
+
+ TTagRegistry Tags_;
+ TProducerSet Producers_;
+
+ THashMap<TString, TSensorSet> Sensors_;
+
+ TSensorSet* FindSet(const TString& name, const TSensorOptions& options);
+
+ TCounter RegistrationCount_;
+ TEventTimer SensorCollectDuration_, ReadDuration_;
+ TGauge SensorCount_, ProjectionCount_, TagCount_;
+
+ friend class TRemoteRegistry;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSolomonRegistry)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/remote.cpp b/yt/yt/library/profiling/solomon/remote.cpp
new file mode 100644
index 0000000000..5e177de31e
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/remote.cpp
@@ -0,0 +1,212 @@
+#include "remote.h"
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TSummaryDouble* proto, const TSummarySnapshot<double>& summary)
+{
+ proto->set_sum(summary.Sum());
+ proto->set_min(summary.Min());
+ proto->set_max(summary.Max());
+ proto->set_last(summary.Last());
+ proto->set_count(summary.Count());
+}
+
+void FromProto(TSummarySnapshot<double>* summary, const NProto::TSummaryDouble& proto)
+{
+ *summary = TSummarySnapshot<double>(
+ proto.sum(),
+ proto.min(),
+ proto.max(),
+ proto.last(),
+ proto.count()
+ );
+}
+
+void ToProto(NProto::TSummaryDuration* proto, const TSummarySnapshot<TDuration>& summary)
+{
+ proto->set_sum(summary.Sum().GetValue());
+ proto->set_min(summary.Min().GetValue());
+ proto->set_max(summary.Max().GetValue());
+ proto->set_last(summary.Last().GetValue());
+ proto->set_count(summary.Count());
+}
+
+void FromProto(TSummarySnapshot<TDuration>* summary, const NProto::TSummaryDuration& proto)
+{
+ *summary = TSummarySnapshot<TDuration>(
+ TDuration::FromValue(proto.sum()),
+ TDuration::FromValue(proto.min()),
+ TDuration::FromValue(proto.max()),
+ TDuration::FromValue(proto.last()),
+ proto.count()
+ );
+}
+
+void ToProto(NProto::THistogramSnapshot* proto, const THistogramSnapshot& histogram)
+{
+ for (auto time : histogram.Bounds) {
+ proto->add_times(TDuration::Seconds(time).GetValue());
+ }
+ for (auto value : histogram.Values) {
+ proto->add_values(value);
+ }
+}
+
+void FromProto(THistogramSnapshot* histogram, const NProto::THistogramSnapshot& proto)
+{
+ histogram->Values.clear();
+ histogram->Bounds.clear();
+
+ for (auto time : proto.times()) {
+ histogram->Bounds.push_back(TDuration::FromValue(time).SecondsFloat());
+ }
+
+ for (auto value : proto.values()) {
+ histogram->Values.push_back(value);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRemoteRegistry::TRemoteRegistry(TSolomonRegistry* registry)
+ : Registry_(registry)
+{
+ TagRename_.emplace_back();
+}
+
+void TRemoteRegistry::Transfer(const NProto::TSensorDump& dump)
+{
+ for (const auto& cube : dump.cubes()) {
+ for (const auto& projection : cube.projections()) {
+ for (const auto& tagId : projection.tag_ids()) {
+ if (tagId <= 0 || tagId > dump.tags().size()) {
+ THROW_ERROR_EXCEPTION("Incorrect tag")
+ << TErrorAttribute("tag_id", tagId);
+ }
+ }
+ }
+ }
+
+ for (TTagId tagId = TagRename_.size(); tagId < dump.tags().size(); tagId++) {
+ const auto& remoteTag = dump.tags()[tagId];
+ TagRename_.push_back(Registry_->Tags_.Encode(TTag{remoteTag.key(), remoteTag.value()}));
+ }
+
+ auto oldSensors = std::move(Sensors_);
+ Sensors_ = {};
+
+ for (const auto& cube : dump.cubes()) {
+ TSensorOptions options;
+ options.Sparse = cube.sparse();
+ options.Global = cube.global();
+ options.DisableSensorsRename = cube.disable_sensors_rename();
+ options.DisableDefault = cube.disable_default();
+
+ auto sensorName = cube.name();
+ auto sensorSet = Registry_->FindSet(cube.name(), options);
+ auto& usedTags = Sensors_[cube.name()];
+
+ for (const auto& projection : cube.projections()) {
+ TTagIdList tagIds;
+ for (const auto& tagId : projection.tag_ids()) {
+ tagIds.push_back(tagId);
+ }
+ tagIds = RenameTags(tagIds);
+
+ auto transferValue = [&] (auto cube, ESensorType type, auto value) {
+ sensorSet->InitializeType(type);
+
+ bool inserted = usedTags.UsedTags.emplace(type, tagIds).second;
+ if (inserted) {
+ cube->Add(tagIds);
+ }
+
+ if (projection.has_value()) {
+ cube->Update(tagIds, value);
+ }
+ };
+
+ if (projection.has_counter()) {
+ transferValue(&sensorSet->CountersCube_, ESensorType::Counter, projection.counter());
+ } else if (projection.has_duration()) {
+ transferValue(&sensorSet->TimeCountersCube_, ESensorType::TimeCounter, TDuration::FromValue(projection.duration()));
+ } else if (projection.has_gauge()) {
+ transferValue(&sensorSet->GaugesCube_, ESensorType::Gauge, projection.gauge());
+ } else if (projection.has_summary()) {
+ transferValue(&sensorSet->SummariesCube_, ESensorType::Summary, NYT::FromProto<TSummarySnapshot<double>>(projection.summary()));
+ } else if (projection.has_timer()) {
+ transferValue(&sensorSet->TimersCube_, ESensorType::Timer, NYT::FromProto<TSummarySnapshot<TDuration>>(projection.timer()));
+ } else if (projection.has_time_histogram()) {
+ transferValue(&sensorSet->TimeHistogramsCube_, ESensorType::TimeHistogram, NYT::FromProto<TTimeHistogramSnapshot>(projection.time_histogram()));
+ } else if (projection.has_gauge_histogram()) {
+ transferValue(&sensorSet->GaugeHistogramsCube_, ESensorType::GaugeHistogram, NYT::FromProto<TGaugeHistogramSnapshot>(projection.gauge_histogram()));
+ } else if (projection.has_rate_histogram()) {
+ transferValue(&sensorSet->RateHistogramsCube_, ESensorType::RateHistogram, NYT::FromProto<TRateHistogramSnapshot>(projection.rate_histogram()));
+ } else {
+ // Ignore unknown types.
+ }
+ }
+ }
+
+ DoDetach(oldSensors);
+}
+
+void TRemoteRegistry::Detach()
+{
+ DoDetach(Sensors_);
+}
+
+void TRemoteRegistry::DoDetach(const THashMap<TString, TRemoteSensorSet>& sensors)
+{
+ for (const auto& [name, usedTags] : sensors) {
+ auto& sensorSet = Registry_->Sensors_.find(name)->second;
+
+ for (const auto& [type, tags] : usedTags.UsedTags) {
+ switch (type) {
+ case ESensorType::Counter:
+ sensorSet.CountersCube_.Remove(tags);
+ break;
+ case ESensorType::TimeCounter:
+ sensorSet.TimeCountersCube_.Remove(tags);
+ break;
+ case ESensorType::Gauge:
+ sensorSet.GaugesCube_.Remove(tags);
+ break;
+ case ESensorType::Summary:
+ sensorSet.SummariesCube_.Remove(tags);
+ break;
+ case ESensorType::Timer:
+ sensorSet.TimersCube_.Remove(tags);
+ break;
+ case ESensorType::TimeHistogram:
+ sensorSet.TimeHistogramsCube_.Remove(tags);
+ break;
+ case ESensorType::GaugeHistogram:
+ sensorSet.GaugeHistogramsCube_.Remove(tags);
+ break;
+ case ESensorType::RateHistogram:
+ sensorSet.RateHistogramsCube_.Remove(tags);
+ break;
+ default:
+ YT_ABORT();
+ }
+ }
+ }
+}
+
+TTagIdList TRemoteRegistry::RenameTags(const TTagIdList& tags)
+{
+ TTagIdList renamed;
+ for (auto tag : tags) {
+ renamed.push_back(TagRename_[tag]);
+ }
+ return renamed;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/remote.h b/yt/yt/library/profiling/solomon/remote.h
new file mode 100644
index 0000000000..db28348416
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/remote.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "registry.h"
+
+#include <yt/yt/library/profiling/tag.h>
+#include <yt/yt/library/profiling/summary.h>
+#include <yt/yt/library/profiling/histogram_snapshot.h>
+#include <yt/yt/library/profiling/solomon/sensor_dump.pb.h>
+
+#include <util/generic/hash_set.h>
+
+#include <deque>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TSummaryDouble* proto, const TSummarySnapshot<double>& summary);
+void FromProto(TSummarySnapshot<double>* summary, const NProto::TSummaryDouble& proto);
+
+void ToProto(NProto::TSummaryDuration* proto, const TSummarySnapshot<TDuration>& summary);
+void FromProto(TSummarySnapshot<TDuration>* summary, const NProto::TSummaryDuration& proto);
+
+void ToProto(NProto::THistogramSnapshot* proto, const THistogramSnapshot& histogram);
+void FromProto(THistogramSnapshot* histogram, const NProto::THistogramSnapshot& proto);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoteRegistry final
+{
+public:
+ explicit TRemoteRegistry(TSolomonRegistry* registry);
+ void Transfer(const NProto::TSensorDump& dump);
+ void Detach();
+
+private:
+ TSolomonRegistry* Registry_ = nullptr;
+
+ std::deque<TTagId> TagRename_;
+
+ struct TRemoteSensorSet
+ {
+ THashSet<std::pair<ESensorType, TTagIdList>> UsedTags;
+ };
+
+ THashMap<TString, TRemoteSensorSet> Sensors_;
+
+ TTagIdList RenameTags(const TTagIdList& tags);
+ void DoDetach(const THashMap<TString, TRemoteSensorSet>& sensors);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/sensor.cpp b/yt/yt/library/profiling/solomon/sensor.cpp
new file mode 100644
index 0000000000..f2e74531a2
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/sensor.cpp
@@ -0,0 +1,268 @@
+#include "sensor.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <atomic>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_REFCOUNTED_TYPE(TSimpleCounter)
+DEFINE_REFCOUNTED_TYPE(TSimpleGauge)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSimpleGauge::Update(double value)
+{
+ Value_.store(value, std::memory_order::relaxed);
+}
+
+double TSimpleGauge::GetValue()
+{
+ return Value_.load(std::memory_order::relaxed);
+}
+
+void TSimpleGauge::Record(double /*value*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TSummarySnapshot<double> TSimpleGauge::GetSummary()
+{
+ TSummarySnapshot<double> summary;
+ summary.Record(GetValue());
+ return summary;
+}
+
+TSummarySnapshot<double> TSimpleGauge::GetSummaryAndReset()
+{
+ return GetSummary();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSimpleTimeGauge::Update(TDuration value)
+{
+ Value_.store(value.GetValue(), std::memory_order::relaxed);
+}
+
+TDuration TSimpleTimeGauge::GetValue()
+{
+ return TDuration::FromValue(Value_.load(std::memory_order::relaxed));
+}
+
+void TSimpleTimeGauge::Record(TDuration /*value*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TSummarySnapshot<TDuration> TSimpleTimeGauge::GetSummary()
+{
+ TSummarySnapshot<TDuration> summary;
+ summary.Record(GetValue());
+ return summary;
+}
+
+TSummarySnapshot<TDuration> TSimpleTimeGauge::GetSummaryAndReset()
+{
+ return GetSummary();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSimpleCounter::Increment(i64 delta)
+{
+ YT_VERIFY(delta >= 0);
+ Value_.fetch_add(delta, std::memory_order::relaxed);
+}
+
+i64 TSimpleCounter::GetValue()
+{
+ return Value_.load(std::memory_order::relaxed);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSimpleTimeCounter::Add(TDuration delta)
+{
+ Value_.fetch_add(delta.GetValue(), std::memory_order::relaxed);
+}
+
+TDuration TSimpleTimeCounter::GetValue()
+{
+ return TDuration::FromValue(Value_.load(std::memory_order::relaxed));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TSimpleSummary<T>::Record(T value)
+{
+ auto guard = Guard(Lock_);
+ Value_.Record(value);
+}
+
+template <class T>
+TSummarySnapshot<T> TSimpleSummary<T>::GetSummary()
+{
+ auto guard = Guard(Lock_);
+ return Value_;
+}
+
+template <class T>
+TSummarySnapshot<T> TSimpleSummary<T>::GetSummaryAndReset()
+{
+ auto guard = Guard(Lock_);
+
+ auto value = Value_;
+ Value_ = {};
+ return value;
+}
+
+template class TSimpleSummary<double>;
+template class TSimpleSummary<TDuration>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int MaxBinCount = 65;
+
+static auto GenericBucketBounds()
+{
+ std::array<ui64, MaxBinCount> result;
+
+ for (int index = 0; index <= 6; ++index) {
+ result[index] = 1ull << index;
+ }
+
+ for (int index = 7; index < 10; ++index) {
+ result[index] = 1000ull >> (10 - index);
+ }
+
+ for (int index = 10; index < MaxBinCount; ++index) {
+ result[index] = 1000 * result[index - 10];
+ }
+
+ return result;
+}
+
+std::vector<double> GenerateGenericBucketBounds()
+{
+ // BEWARE: Changing this variable will lead to master snapshots becoming invalid.
+ constexpr int MaxHistogramBinCount = 38;
+ std::vector<double> result;
+
+ auto genericBounds = GenericBucketBounds();
+ result.reserve(MaxHistogramBinCount);
+
+ for (int i = 0; i < std::ssize(genericBounds) && i < MaxHistogramBinCount; ++i) {
+ result.push_back(genericBounds[i]);
+ }
+
+ return result;
+}
+
+static std::vector<double> BucketBounds(const TSensorOptions& options)
+{
+ if (!options.HistogramBounds.empty()) {
+ return options.HistogramBounds;
+ }
+
+ std::vector<double> bounds;
+ if (!options.TimeHistogramBounds.empty()) {
+ for (auto b : options.TimeHistogramBounds) {
+ bounds.push_back(b.SecondsFloat());
+ }
+ return bounds;
+ }
+
+ if (options.HistogramMin.Zero() && options.HistogramMax.Zero()) {
+ return {};
+ }
+
+ for (auto bound : GenericBucketBounds()) {
+ auto duration = TDuration::FromValue(bound);
+ if (options.HistogramMin <= duration && duration <= options.HistogramMax) {
+ bounds.push_back(duration.SecondsFloat());
+ }
+ }
+
+ return bounds;
+}
+
+THistogram::THistogram(const TSensorOptions& options)
+ : Bounds_(BucketBounds(options))
+ , Buckets_(Bounds_.size() + 1)
+{
+ YT_VERIFY(!Bounds_.empty());
+ YT_VERIFY(Bounds_.size() <= MaxBinCount);
+}
+
+void THistogram::Record(TDuration value)
+{
+ auto it = std::lower_bound(Bounds_.begin(), Bounds_.end(), value.SecondsFloat());
+ Buckets_[it - Bounds_.begin()].fetch_add(1, std::memory_order::relaxed);
+}
+
+void THistogram::Add(double value, int count) noexcept
+{
+ auto it = std::lower_bound(Bounds_.begin(), Bounds_.end(), value);
+ Buckets_[it - Bounds_.begin()].fetch_add(count, std::memory_order::relaxed);
+}
+
+void THistogram::Remove(double value, int count) noexcept
+{
+ auto it = std::lower_bound(Bounds_.begin(), Bounds_.end(), value);
+ Buckets_[it - Bounds_.begin()].fetch_sub(count, std::memory_order::relaxed);
+}
+
+void THistogram::Reset() noexcept
+{
+ for (int i = 0; i < std::ssize(Buckets_); ++i) {
+ Buckets_[i] = 0;
+ }
+}
+
+THistogramSnapshot THistogram::GetSnapshot(bool reset)
+{
+ THistogramSnapshot snapshot;
+ snapshot.Bounds = Bounds_;
+ snapshot.Values.resize(Buckets_.size());
+
+ for (int i = 0; i < std::ssize(Buckets_); ++i) {
+ if (!reset) {
+ snapshot.Values[i] = Buckets_[i].load(std::memory_order::relaxed);
+ } else {
+ snapshot.Values[i] = Buckets_[i].exchange(0, std::memory_order::relaxed);
+ }
+ }
+
+ return snapshot;
+}
+
+void THistogram::LoadSnapshot(THistogramSnapshot snapshot)
+{
+ for (int i = 0; i < std::ssize(snapshot.Bounds); ++i) {
+ YT_VERIFY(Bounds_[i] == snapshot.Bounds[i]);
+ }
+
+ YT_VERIFY(std::ssize(Buckets_) == std::ssize(Bounds_) + 1);
+
+ for (int i = 0; i < std::ssize(snapshot.Values); ++i) {
+ Buckets_[i].store(snapshot.Values[i], std::memory_order::relaxed);
+ }
+}
+
+TSummarySnapshot<TDuration> THistogram::GetSummary()
+{
+ YT_UNIMPLEMENTED();
+}
+
+TSummarySnapshot<TDuration> THistogram::GetSummaryAndReset()
+{
+ YT_UNIMPLEMENTED();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/sensor.h b/yt/yt/library/profiling/solomon/sensor.h
new file mode 100644
index 0000000000..3ac491ef59
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/sensor.h
@@ -0,0 +1,135 @@
+#pragma once
+
+#include <yt/yt/library/profiling/impl.h>
+#include <yt/yt/library/profiling/summary.h>
+#include <yt/yt/library/profiling/histogram_snapshot.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleGauge
+ : public IGaugeImpl
+ , public ISummaryImpl
+{
+public:
+ void Update(double value) override;
+
+ double GetValue() override;
+
+ void Record(double value) override;
+
+ TSummarySnapshot<double> GetSummary() override;
+ TSummarySnapshot<double> GetSummaryAndReset() override;
+
+private:
+ std::atomic<double> Value_ = 0.0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleTimeGauge
+ : public ITimeGaugeImpl
+ , public ITimerImpl
+{
+public:
+ void Update(TDuration value) override;
+
+ TDuration GetValue() override;
+
+ void Record(TDuration value) override;
+
+ TSummarySnapshot<TDuration> GetSummary() override;
+ TSummarySnapshot<TDuration> GetSummaryAndReset() override;
+
+private:
+ std::atomic<TDuration::TValue> Value_ = 0.0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleCounter
+ : public ICounterImpl
+{
+public:
+ void Increment(i64 delta) override;
+
+ i64 GetValue() override;
+
+private:
+ std::atomic<i64> Value_ = 0;
+};
+
+static_assert(sizeof(TSimpleCounter) == 24);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleTimeCounter
+ : public ITimeCounterImpl
+{
+public:
+ void Add(TDuration delta) override;
+
+ TDuration GetValue() override;
+
+private:
+ std::atomic<TDuration::TValue> Value_{0};
+};
+
+static_assert(sizeof(TSimpleTimeCounter) == 24);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TSimpleSummary
+ : public ISummaryImplBase<T>
+{
+public:
+ void Record(T value) override;
+
+ TSummarySnapshot<T> GetSummary() override;
+ TSummarySnapshot<T> GetSummaryAndReset() override;
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ TSummarySnapshot<T> Value_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(THistogram)
+
+std::vector<double> GenerateGenericBucketBounds();
+
+class THistogram
+ : public ISummaryImplBase<TDuration>
+ , public IHistogramImpl
+{
+public:
+ THistogram(const TSensorOptions& options);
+
+ void Record(TDuration value) override;
+
+ void Add(double value, int count) noexcept override;
+ void Remove(double value, int count) noexcept override;
+ void Reset() noexcept override;
+
+ THistogramSnapshot GetSnapshot(bool reset) override;
+ void LoadSnapshot(THistogramSnapshot snapshot) override;
+
+private:
+ std::vector<double> Bounds_;
+ std::vector<std::atomic<int>> Buckets_;
+
+ // These two methods are not used.
+ TSummarySnapshot<TDuration> GetSummary() override;
+ TSummarySnapshot<TDuration> GetSummaryAndReset() override;
+};
+
+DEFINE_REFCOUNTED_TYPE(THistogram)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/sensor_dump.proto b/yt/yt/library/profiling/solomon/sensor_dump.proto
new file mode 100644
index 0000000000..40d775c6bb
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/sensor_dump.proto
@@ -0,0 +1,57 @@
+package NYT.NProfiling.NProto;
+
+message TSensorDump {
+ repeated TTag tags = 2;
+ repeated TCube cubes = 1;
+};
+
+message TTag {
+ required string key = 1;
+ required string value = 2;
+};
+
+message TCube {
+ required string name = 1;
+ repeated TProjection projections = 2;
+
+ optional bool sparse = 3 [default=false];
+ optional bool global = 4 [default=false];
+ optional bool disable_sensors_rename = 5 [default=false];
+ optional bool disable_default = 6 [default=false];
+};
+
+message TProjection {
+ repeated int64 tag_ids = 1;
+
+ required bool has_value = 8;
+
+ optional double gauge = 2;
+ optional int64 counter = 3;
+ optional int64 duration = 4;
+ optional TSummaryDouble summary = 5;
+ optional TSummaryDuration timer = 6;
+ optional THistogramSnapshot time_histogram = 7;
+ optional THistogramSnapshot gauge_histogram = 9;
+ optional THistogramSnapshot rate_histogram = 10;
+};
+
+message TSummaryDouble {
+ required double sum = 1;
+ required double min = 2;
+ required double max = 3;
+ required double last = 4;
+ required int64 count = 5;
+};
+
+message TSummaryDuration {
+ required int64 sum = 1;
+ required int64 min = 2;
+ required int64 max = 3;
+ required int64 last = 4;
+ required int64 count = 5;
+};
+
+message THistogramSnapshot {
+ repeated int64 values = 1;
+ repeated int64 times = 2;
+};
diff --git a/yt/yt/library/profiling/solomon/sensor_service.cpp b/yt/yt/library/profiling/solomon/sensor_service.cpp
new file mode 100644
index 0000000000..04de53b703
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/sensor_service.cpp
@@ -0,0 +1,283 @@
+#include "sensor_service.h"
+#include "cube.h"
+#include "exporter.h"
+#include "registry.h"
+#include "private.h"
+
+#include <yt/yt/core/concurrency/async_rw_lock.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/virtual.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/ypath_detail.h>
+
+namespace NYT::NProfiling {
+
+using namespace NConcurrency;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const static auto& Logger = SolomonLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSensorServiceImpl
+ : public TYPathServiceBase
+ , public TSupportsGet
+{
+public:
+ TSensorServiceImpl(
+ TString name,
+ TSolomonRegistry* registry,
+ TAsyncReaderWriterLock* const exporterLock)
+ : Name_(std::move(name))
+ , Registry_(std::move(registry))
+ , ExporterLock_(exporterLock)
+ { }
+
+private:
+ using TTagMap = THashMap<TString, TString>;
+
+ const TString Name_;
+ TSolomonRegistry* const Registry_;
+ TAsyncReaderWriterLock* const ExporterLock_;
+
+ struct TGetSensorOptions
+ {
+ std::optional<TString> Name;
+ TTagMap TagMap;
+ bool ReadAllProjections = false;
+ bool ExportSummaryAsMax = true;
+ bool SummaryAsMaxForAllTime = false;
+ };
+
+ TGetSensorOptions ParseGetSensorOptions(TReqGet* request) const
+ {
+ auto requestOptions = NYTree::FromProto(request->options());
+
+ TGetSensorOptions options;
+ // Set default value depending on whether we are in the sensor service root.
+ bool defaultReadAllProjections;
+ if (Name_) {
+ THROW_ERROR_EXCEPTION_IF(requestOptions->Contains("name"),
+ "Specifying \"name\" option is allowed only in requests to sensor service root");
+
+ defaultReadAllProjections = true;
+ options.Name = Name_;
+ } else {
+ defaultReadAllProjections = false;
+ options.Name = requestOptions->Find<TString>("name");
+ }
+
+ options.ReadAllProjections = requestOptions->Get<bool>("read_all_projections", /*defaultValue*/ defaultReadAllProjections);
+ options.TagMap = requestOptions->Get<TTagMap>("tags", /*defaultValue*/ TTagMap{});
+ options.ExportSummaryAsMax = requestOptions->Get<bool>("export_summary_as_max", /*defaultValue*/ true);
+ options.SummaryAsMaxForAllTime = requestOptions->Get<bool>("summary_as_max_for_all_time", /*defaultValue*/ false);
+
+ return options;
+ }
+
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override
+ {
+ auto options = ParseGetSensorOptions(request);
+ YT_LOG_DEBUG("Received sensor value request (RequestId: %v, Name: %v, Tags: %v, ReadAllProjections: %v, ExportSummaryAsMax: %v)",
+ context->GetRequestId(),
+ options.Name,
+ options.TagMap,
+ options.ReadAllProjections,
+ options.ExportSummaryAsMax);
+
+ if (!options.Name) {
+ response->set_value(BuildYsonStringFluently().Entity().ToString());
+ context->Reply();
+ return;
+ }
+
+ auto guard = WaitFor(TAsyncLockReaderGuard::Acquire(ExporterLock_))
+ .ValueOrThrow();
+
+ TTagList tags(options.TagMap.begin(), options.TagMap.end());
+ TReadOptions readOptions{
+ .ExportSummaryAsMax = options.ExportSummaryAsMax,
+ .ReadAllProjections = options.ReadAllProjections,
+ .SummaryAsMaxForAllTime = options.SummaryAsMaxForAllTime,
+ };
+ response->set_value(BuildYsonStringFluently()
+ .Do([&] (TFluentAny fluent) {
+ Registry_->ReadRecentSensorValues(*options.Name, tags, readOptions, fluent);
+ }).ToString());
+ context->Reply();
+ }
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ return TYPathServiceBase::DoInvoke(context);
+ }
+};
+
+using TSensorServiceImplPtr = TIntrusivePtr<TSensorServiceImpl>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSensorService
+ : public TYPathServiceBase
+ , public TSupportsList
+{
+public:
+ TSensorService(
+ TSolomonExporterConfigPtr config,
+ TSolomonRegistryPtr registry,
+ TSolomonExporterPtr exporter)
+ : Config_(std::move(config))
+ , Registry_(std::move(registry))
+ , Exporter_(std::move(exporter))
+ , RootSensorServiceImpl_(New<TSensorServiceImpl>(/*name*/ TString(), Registry_.Get(), &Exporter_->Lock_))
+ , Root_(GetEphemeralNodeFactory(/*shouldHideAttributes*/ true)->CreateMap())
+ , SensorTreeUpdateDuration_(Registry_->GetSelfProfiler().Timer("/sensor_service_tree_update_duration"))
+ {
+ UpdateSensorTreeExecutor_ = New<TPeriodicExecutor>(
+ Exporter_->ControlQueue_->GetInvoker(),
+ BIND(&TSensorService::UpdateSensorTree, MakeWeak(this)),
+ Config_->UpdateSensorServiceTreePeriod);
+ UpdateSensorTreeExecutor_->Start();
+ }
+
+private:
+ const TSolomonExporterConfigPtr Config_;
+ const TSolomonRegistryPtr Registry_;
+ const TSolomonExporterPtr Exporter_;
+ const TSensorServiceImplPtr RootSensorServiceImpl_;
+ const IMapNodePtr Root_;
+
+ THashMap<TString, TSensorServiceImplPtr> NameToSensorServiceImpl_;
+
+ TEventTimer SensorTreeUpdateDuration_;
+ TPeriodicExecutorPtr UpdateSensorTreeExecutor_;
+
+ void UpdateSensorTree()
+ {
+ YT_LOG_DEBUG("Updating sensor service tree");
+
+ TWallTimer timer;
+
+ int addedSensorCount = 0;
+ int malformedSensorCount = 0;
+ auto sensors = Registry_->ListSensors();
+ for (const auto& sensorInfo : sensors) {
+ const auto& name = sensorInfo.Name;
+
+ if (NameToSensorServiceImpl_.contains(name)) {
+ continue;
+ }
+
+ auto sensorServiceImpl = New<TSensorServiceImpl>(name, Registry_.Get(), &Exporter_->Lock_);
+ EmplaceOrCrash(NameToSensorServiceImpl_, name, sensorServiceImpl);
+
+ auto node = CreateVirtualNode(std::move(sensorServiceImpl));
+ auto path = "/" + name;
+ try {
+ ForceYPath(Root_, path);
+ SetNodeByYPath(Root_, path, node);
+ } catch (const std::exception& ex) {
+ YT_LOG_DEBUG(ex, "Failed to add new sensor to the sensor service tree (Name: %v)", name);
+
+ // Ignore sensors with weird names.
+ ++malformedSensorCount;
+ continue;
+ }
+
+ ++addedSensorCount;
+ }
+
+ auto elapsed = timer.GetElapsedTime();
+ SensorTreeUpdateDuration_.Record(elapsed);
+
+ YT_LOG_DEBUG(
+ "Finished updating sensor service tree"
+ "(TotalSensorCount: %v, AddedSensorCount: %v, MalformedSensorCount: %v, Elapsed: %v)",
+ sensors.size(),
+ addedSensorCount,
+ malformedSensorCount,
+ elapsed);
+ }
+
+ IYPathService::TResolveResult ResolveSelf(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override
+ {
+ if (context->GetMethod() == "List") {
+ return TResolveResultHere{path};
+ }
+ return TResolveResultThere{RootSensorServiceImpl_, path};
+ }
+
+ IYPathService::TResolveResult ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/) override
+ {
+ return TResolveResultThere{Root_, "/" + path};
+ }
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ DISPATCH_YPATH_SERVICE_METHOD(List);
+ return TYPathServiceBase::DoInvoke(context);
+ }
+
+ void ListSelf(TReqList* request, TRspList* response, const TCtxListPtr& context) override
+ {
+ auto guard = WaitFor(TAsyncLockReaderGuard::Acquire(&Exporter_->Lock_))
+ .ValueOrThrow();
+
+ auto attributeKeys = NYT::FromProto<THashSet<TString>>(request->attributes().keys());
+ context->SetRequestInfo("AttributeKeys: %v", attributeKeys);
+
+ response->set_value(BuildYsonStringFluently()
+ .DoListFor(Registry_->ListSensors(), [&] (TFluentList fluent, const TSensorInfo& sensorInfo) {
+ if (!sensorInfo.Error.IsOK()) {
+ THROW_ERROR_EXCEPTION("Broken sensor")
+ << TErrorAttribute("name", sensorInfo.Name)
+ << sensorInfo.Error;
+ }
+
+ fluent
+ .Item().Do([&] (TFluentAny fluent) {
+ if (!attributeKeys.empty()) {
+ fluent
+ .BeginAttributes()
+ .DoIf(attributeKeys.contains("cube_size"), [&] (TFluentMap fluent) {
+ fluent.Item("cube_size").Value(sensorInfo.CubeSize);
+ })
+ .DoIf(attributeKeys.contains("object_count"), [&] (TFluentMap fluent) {
+ fluent.Item("object_count").Value(sensorInfo.CubeSize);
+ })
+ .EndAttributes();
+ }
+
+ fluent.Value(sensorInfo.Name);
+ });
+ }).ToString());
+
+ context->Reply();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IYPathServicePtr CreateSensorService(
+ TSolomonExporterConfigPtr config,
+ TSolomonRegistryPtr registry,
+ TSolomonExporterPtr exporter)
+{
+ return New<TSensorService>(
+ std::move(config),
+ std::move(registry),
+ std::move(exporter));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/sensor_service.h b/yt/yt/library/profiling/solomon/sensor_service.h
new file mode 100644
index 0000000000..2786ab1aa9
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/sensor_service.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NYTree::IYPathServicePtr CreateSensorService(
+ TSolomonExporterConfigPtr config,
+ TSolomonRegistryPtr registry,
+ TSolomonExporterPtr exporter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/sensor_set.cpp b/yt/yt/library/profiling/solomon/sensor_set.cpp
new file mode 100644
index 0000000000..1e9fa0ba5e
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/sensor_set.cpp
@@ -0,0 +1,416 @@
+#include "sensor_set.h"
+#include "private.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/monlib/metrics/summary_snapshot.h>
+
+namespace NYT::NProfiling {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const static auto& Logger = SolomonLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSensorSet::TSensorSet(TSensorOptions options, i64 iteration, int windowSize, int gridFactor)
+ : Options_(options)
+ , GridFactor_(gridFactor)
+ , CountersCube_{windowSize, iteration}
+ , TimeCountersCube_{windowSize, iteration}
+ , GaugesCube_{windowSize, iteration}
+ , SummariesCube_{windowSize, iteration}
+ , TimersCube_{windowSize, iteration}
+ , TimeHistogramsCube_{windowSize, iteration}
+ , GaugeHistogramsCube_{windowSize, iteration}
+ , RateHistogramsCube_{windowSize, iteration}
+{ }
+
+bool TSensorSet::IsEmpty() const
+{
+ return Counters_.empty() &&
+ Gauges_.empty() &&
+ Summaries_.empty() &&
+ Timers_.empty() &&
+ TimeHistograms_.empty() &&
+ GaugeHistograms_.empty() &&
+ RateHistograms_.empty();
+}
+
+void TSensorSet::Profile(const TProfiler &profiler)
+{
+ CubeSize_ = profiler.Gauge("/cube_size");
+ SensorsEmitted_ = profiler.Gauge("/sensors_emitted");
+}
+
+void TSensorSet::ValidateOptions(TSensorOptions options)
+{
+ if (!Options_.IsCompatibleWith(options)) {
+ OnError(TError("Conflicting sensor settings")
+ << TErrorAttribute("current", ToString(Options_))
+ << TErrorAttribute("provided", ToString(options)));
+ }
+}
+
+void TSensorSet::AddCounter(TCounterStatePtr counter)
+{
+ InitializeType(ESensorType::Counter);
+ CountersCube_.AddAll(counter->TagIds, counter->Projections);
+ Counters_.emplace(std::move(counter));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::AddGauge(TGaugeStatePtr gauge)
+{
+ InitializeType(ESensorType::Gauge);
+ GaugesCube_.AddAll(gauge->TagIds, gauge->Projections);
+ Gauges_.emplace(std::move(gauge));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::AddSummary(TSummaryStatePtr summary)
+{
+ InitializeType(ESensorType::Summary);
+ SummariesCube_.AddAll(summary->TagIds, summary->Projections);
+ Summaries_.emplace(std::move(summary));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::AddTimerSummary(TTimerSummaryStatePtr timer)
+{
+ InitializeType(ESensorType::Timer);
+ TimersCube_.AddAll(timer->TagIds, timer->Projections);
+ Timers_.emplace(std::move(timer));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::AddTimeCounter(TTimeCounterStatePtr counter)
+{
+ InitializeType(ESensorType::TimeCounter);
+ TimeCountersCube_.AddAll(counter->TagIds, counter->Projections);
+ TimeCounters_.emplace(std::move(counter));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::AddTimeHistogram(THistogramStatePtr histogram)
+{
+ InitializeType(ESensorType::TimeHistogram);
+ TimeHistogramsCube_.AddAll(histogram->TagIds, histogram->Projections);
+ TimeHistograms_.emplace(std::move(histogram));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::AddGaugeHistogram(THistogramStatePtr histogram)
+{
+ InitializeType(ESensorType::GaugeHistogram);
+ GaugeHistogramsCube_.AddAll(histogram->TagIds, histogram->Projections);
+ GaugeHistograms_.emplace(std::move(histogram));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::AddRateHistogram(THistogramStatePtr histogram)
+{
+ InitializeType(ESensorType::RateHistogram);
+ RateHistogramsCube_.AddAll(histogram->TagIds, histogram->Projections);
+ RateHistograms_.emplace(std::move(histogram));
+ CubeSize_.Update(GetCubeSize());
+}
+
+void TSensorSet::RenameDynamicTag(const TDynamicTagPtr& dynamicTag, TTagId newTag)
+{
+ auto doRename = [&] (auto& cube, const auto& sensors) {
+ for (const auto& sensor : sensors) {
+ for (auto [tag, index] : sensor->Projections.DynamicTags()) {
+ if (tag == dynamicTag) {
+ cube.RemoveAll(sensor->TagIds, sensor->Projections);
+ sensor->TagIds[index] = newTag;
+ cube.AddAll(sensor->TagIds, sensor->Projections);
+ }
+ }
+ }
+ };
+
+ doRename(CountersCube_, Counters_);
+ doRename(GaugesCube_, Gauges_);
+ doRename(SummariesCube_, Summaries_);
+ doRename(TimersCube_, Timers_);
+ doRename(TimeHistogramsCube_, TimeHistograms_);
+ doRename(GaugeHistogramsCube_, GaugeHistograms_);
+ doRename(RateHistogramsCube_, RateHistograms_);
+}
+
+int TSensorSet::Collect()
+{
+ int count = 0;
+
+ auto collect = [&] (auto& set, auto& cube, auto doRead) {
+ using TElement = typename std::remove_reference_t<decltype(set)>::key_type;
+
+ std::deque<TElement> toRemove;
+
+ cube.StartIteration();
+ for (const auto& counter : set) {
+ auto [value, ok] = doRead(counter);
+ if (!ok) {
+ toRemove.push_back(counter);
+ continue;
+ }
+
+ if (Options_.Sparse && IsZeroValue(value)) {
+ continue;
+ }
+
+ counter->Projections.Range(counter->TagIds, [&, value=value] (auto tags) {
+ cube.Update(std::move(tags), value);
+ });
+ }
+ cube.FinishIteration();
+
+ for (const auto& removed : toRemove) {
+ cube.RemoveAll(removed->TagIds, removed->Projections);
+ set.erase(removed);
+ }
+
+ count += cube.GetProjections().size();
+ };
+
+ collect(Counters_, CountersCube_, [] (auto counter) -> std::pair<i64, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {0, false};
+ }
+
+ try {
+ auto value = counter->Reader();
+
+ auto delta = value - counter->LastValue;
+ counter->LastValue = value;
+ return {delta, true};
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Counter read failed");
+ return {0, false};
+ }
+ });
+
+ collect(TimeCounters_, TimeCountersCube_, [] (auto counter) -> std::pair<TDuration, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {TDuration::Zero(), false};
+ }
+
+ auto value = owner->GetValue();
+
+ auto delta = value - counter->LastValue;
+ counter->LastValue = value;
+ return {delta, true};
+ });
+
+ collect(Gauges_, GaugesCube_, [] (auto counter) -> std::pair<double, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {0, false};
+ }
+
+ try {
+ auto value = counter->Reader();
+
+ return {value, true};
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Gauge read failed");
+ return {0, false};
+ }
+ });
+
+ collect(Summaries_, SummariesCube_, [] (auto counter) -> std::pair<TSummarySnapshot<double>, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {{}, false};
+ }
+
+ auto value = owner->GetSummaryAndReset();
+ return {value, true};
+ });
+
+ collect(Timers_, TimersCube_, [] (auto counter) -> std::pair<TSummarySnapshot<TDuration>, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {{}, false};
+ }
+
+ auto value = owner->GetSummaryAndReset();
+ return {value, true};
+ });
+
+ collect(TimeHistograms_, TimeHistogramsCube_, [] (auto counter) -> std::pair<TTimeHistogramSnapshot, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {{}, false};
+ }
+
+ auto value = owner->GetSnapshot(true);
+ return {value, true};
+ });
+
+ collect(GaugeHistograms_, GaugeHistogramsCube_, [] (auto counter) -> std::pair<TGaugeHistogramSnapshot, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {{}, false};
+ }
+
+ auto value = owner->GetSnapshot(false);
+ return {value, true};
+ });
+
+ collect(RateHistograms_, RateHistogramsCube_, [] (auto counter) -> std::pair<TRateHistogramSnapshot, bool> {
+ auto owner = counter->Owner.Lock();
+ if (!owner) {
+ return {{}, false};
+ }
+
+ auto value = owner->GetSnapshot(true);
+ return {value, true};
+ });
+
+ return count;
+}
+
+void TSensorSet::ReadSensors(
+ const TString& name,
+ const TReadOptions& options,
+ TTagWriter* tagWriter,
+ ::NMonitoring::IMetricConsumer* consumer) const
+{
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ auto readOptions = options;
+ readOptions.Sparse = Options_.Sparse;
+ readOptions.Global = Options_.Global;
+ readOptions.DisableSensorsRename = Options_.DisableSensorsRename;
+ readOptions.DisableDefault = Options_.DisableDefault;
+
+ int sensorsEmitted = 0;
+
+ sensorsEmitted += CountersCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+ sensorsEmitted += TimeCountersCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+ sensorsEmitted += GaugesCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+ sensorsEmitted += SummariesCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+ sensorsEmitted += TimersCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+ sensorsEmitted += TimeHistogramsCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+ sensorsEmitted += GaugeHistogramsCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+ sensorsEmitted += RateHistogramsCube_.ReadSensors(name, readOptions, tagWriter, consumer);
+
+ SensorsEmitted_.Update(sensorsEmitted);
+}
+
+int TSensorSet::ReadSensorValues(
+ const TTagIdList& tagIds,
+ int index,
+ const TReadOptions& options,
+ const TTagRegistry& tagRegistry,
+ TFluentAny fluent) const
+{
+ if (!Error_.IsOK()) {
+ THROW_ERROR_EXCEPTION("Broken sensor")
+ << Error_;
+ }
+
+ int valuesRead = 0;
+ valuesRead += CountersCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+ valuesRead += TimeCountersCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+ valuesRead += GaugesCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+ valuesRead += SummariesCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+ valuesRead += TimersCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+ valuesRead += TimeHistogramsCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+ valuesRead += GaugeHistogramsCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+ valuesRead += RateHistogramsCube_.ReadSensorValues(tagIds, index, options, tagRegistry, fluent);
+
+ return valuesRead;
+}
+
+int TSensorSet::GetGridFactor() const
+{
+ return GridFactor_;
+}
+
+int TSensorSet::GetObjectCount() const
+{
+ return Counters_.size() +
+ TimeCounters_.size() +
+ Gauges_.size() +
+ Summaries_.size() +
+ Timers_.size() +
+ TimeHistograms_.size() +
+ GaugeHistograms_.size() +
+ RateHistograms_.size();
+}
+
+int TSensorSet::GetCubeSize() const
+{
+ return CountersCube_.GetSize() +
+ TimeCountersCube_.GetSize() +
+ GaugesCube_.GetSize() +
+ SummariesCube_.GetSize() +
+ TimersCube_.GetSize() +
+ TimeHistogramsCube_.GetSize() +
+ GaugeHistogramsCube_.GetSize() +
+ RateHistogramsCube_.GetSize();
+}
+
+const TError& TSensorSet::GetError() const
+{
+ return Error_;
+}
+
+std::optional<ESensorType> TSensorSet::GetType() const
+{
+ return Type_;
+}
+
+void TSensorSet::OnError(TError error)
+{
+ if (Error_.IsOK()) {
+ Error_ = std::move(error);
+ }
+}
+
+void TSensorSet::InitializeType(ESensorType type)
+{
+ if (Options_.DisableProjections) {
+ return;
+ }
+
+ if (Type_ && *Type_ != type) {
+ OnError(TError("Conflicting sensor types")
+ << TErrorAttribute("expected", *Type_)
+ << TErrorAttribute("provided", type));
+ }
+
+ if (!Type_) {
+ Type_ = type;
+ }
+}
+
+void TSensorSet::DumpCube(NProto::TCube *cube) const
+{
+ cube->set_sparse(Options_.Sparse);
+ cube->set_global(Options_.Global);
+ cube->set_disable_default(Options_.DisableDefault);
+ cube->set_disable_sensors_rename(Options_.DisableSensorsRename);
+
+ CountersCube_.DumpCube(cube);
+ TimeCountersCube_.DumpCube(cube);
+ GaugesCube_.DumpCube(cube);
+ SummariesCube_.DumpCube(cube);
+ TimersCube_.DumpCube(cube);
+ TimeHistogramsCube_.DumpCube(cube);
+ GaugeHistogramsCube_.DumpCube(cube);
+ RateHistogramsCube_.DumpCube(cube);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/sensor_set.h b/yt/yt/library/profiling/solomon/sensor_set.h
new file mode 100644
index 0000000000..010db9aea8
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/sensor_set.h
@@ -0,0 +1,272 @@
+#pragma once
+
+#include "cube.h"
+#include "tag_registry.h"
+#include "sensor.h"
+
+#include <yt/yt/library/profiling/tag.h>
+#include <yt/yt/library/profiling/solomon/sensor_dump.pb.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/core/misc/intrusive_ptr.h>
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TCounterState)
+
+struct TCounterState final
+{
+ TCounterState(
+ TWeakPtr<TRefCounted> owner,
+ std::function<i64()> reader,
+ const TTagIdList& tagIds,
+ const TProjectionSet& projections)
+ : Owner(std::move(owner))
+ , Reader(std::move(reader))
+ , TagIds(tagIds)
+ , Projections(projections)
+ { }
+
+ const TWeakPtr<TRefCounted> Owner;
+ const std::function<i64()> Reader;
+ i64 LastValue = 0;
+
+ TTagIdList TagIds;
+ const TProjectionSet Projections;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCounterState)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TTimeCounterState)
+
+struct TTimeCounterState final
+{
+ TTimeCounterState(
+ TWeakPtr<ITimeCounterImpl> owner,
+ const TTagIdList& tagIds,
+ const TProjectionSet& projections)
+ : Owner(std::move(owner))
+ , TagIds(tagIds)
+ , Projections(projections)
+ { }
+
+ const TWeakPtr<ITimeCounterImpl> Owner;
+ TDuration LastValue = TDuration::Zero();
+
+ TTagIdList TagIds;
+ const TProjectionSet Projections;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTimeCounterState)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TGaugeState)
+
+struct TGaugeState final
+{
+ TGaugeState(
+ TWeakPtr<TRefCounted> owner,
+ std::function<double()> reader,
+ const TTagIdList& tagIds,
+ const TProjectionSet& projections)
+ : Owner(std::move(owner))
+ , Reader(std::move(reader))
+ , TagIds(tagIds)
+ , Projections(projections)
+ { }
+
+ const TWeakPtr<TRefCounted> Owner;
+ const std::function<double()> Reader;
+
+ TTagIdList TagIds;
+ const TProjectionSet Projections;
+};
+
+DEFINE_REFCOUNTED_TYPE(TGaugeState)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TSummaryState)
+
+struct TSummaryState final
+{
+ TSummaryState(
+ TWeakPtr<ISummaryImpl> owner,
+ const TTagIdList& tagIds,
+ const TProjectionSet& projections)
+ : Owner(std::move(owner))
+ , TagIds(tagIds)
+ , Projections(projections)
+ { }
+
+ const TWeakPtr<ISummaryImpl> Owner;
+
+ TTagIdList TagIds;
+ const TProjectionSet Projections;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSummaryState)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TTimerSummaryState)
+
+struct TTimerSummaryState final
+{
+ TTimerSummaryState(
+ TWeakPtr<ITimerImpl> owner,
+ const TTagIdList& tagIds,
+ const TProjectionSet& projections)
+ : Owner(owner)
+ , TagIds(tagIds)
+ , Projections(projections)
+ { }
+
+ const TWeakPtr<ITimerImpl> Owner;
+
+ TTagIdList TagIds;
+ const TProjectionSet Projections;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTimerSummaryState)
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+DECLARE_REFCOUNTED_STRUCT(THistogramState)
+
+struct THistogramState final
+{
+ THistogramState(
+ TWeakPtr<THistogram> owner,
+ const TTagIdList& tagIds,
+ const TProjectionSet& projections)
+ : Owner(owner)
+ , TagIds(tagIds)
+ , Projections(projections)
+ { }
+
+ const TWeakPtr<THistogram> Owner;
+
+ TTagIdList TagIds;
+ const TProjectionSet Projections;
+};
+
+DEFINE_REFCOUNTED_TYPE(THistogramState)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ESensorType,
+ ((Counter) (1))
+ ((TimeCounter) (2))
+ ((Gauge) (3))
+ ((Summary) (4))
+ ((Timer) (5))
+ ((TimeHistogram) (6))
+ ((GaugeHistogram) (7))
+ ((RateHistogram) (8))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSensorSet
+{
+public:
+ TSensorSet(
+ TSensorOptions options,
+ i64 iteration,
+ int windowSize,
+ int gridFactor);
+
+ bool IsEmpty() const;
+
+ void Profile(const TProfiler& profiler);
+ void ValidateOptions(TSensorOptions options);
+
+ void AddCounter(TCounterStatePtr counter);
+ void AddGauge(TGaugeStatePtr gauge);
+ void AddSummary(TSummaryStatePtr summary);
+ void AddTimerSummary(TTimerSummaryStatePtr timer);
+ void AddTimeCounter(TTimeCounterStatePtr counter);
+ void AddTimeHistogram(THistogramStatePtr histogram);
+ void AddGaugeHistogram(THistogramStatePtr histogram);
+ void AddRateHistogram(THistogramStatePtr histogram);
+
+ void RenameDynamicTag(const TDynamicTagPtr& dynamicTag, TTagId newTag);
+
+ int Collect();
+
+ void ReadSensors(
+ const TString& name,
+ const TReadOptions& options,
+ TTagWriter* tagWriter,
+ ::NMonitoring::IMetricConsumer* consumer) const;
+
+ int ReadSensorValues(
+ const TTagIdList& tagIds,
+ int index,
+ const TReadOptions& options,
+ const TTagRegistry& tagRegistry,
+ NYTree::TFluentAny fluent) const;
+
+ void DumpCube(NProto::TCube* cube) const;
+
+ int GetGridFactor() const;
+ int GetObjectCount() const;
+ int GetCubeSize() const;
+ const TError& GetError() const;
+ std::optional<ESensorType> GetType() const;
+
+private:
+ friend class TRemoteRegistry;
+
+ const TSensorOptions Options_;
+ const int GridFactor_;
+
+ TError Error_;
+
+ THashSet<TCounterStatePtr> Counters_;
+ TCube<i64> CountersCube_;
+
+ THashSet<TTimeCounterStatePtr> TimeCounters_;
+ TCube<TDuration> TimeCountersCube_;
+
+ THashSet<TGaugeStatePtr> Gauges_;
+ TCube<double> GaugesCube_;
+
+ THashSet<TSummaryStatePtr> Summaries_;
+ TCube<TSummarySnapshot<double>> SummariesCube_;
+
+ THashSet<TTimerSummaryStatePtr> Timers_;
+ TCube<TSummarySnapshot<TDuration>> TimersCube_;
+
+ THashSet<THistogramStatePtr> TimeHistograms_;
+ TCube<TTimeHistogramSnapshot> TimeHistogramsCube_;
+
+ THashSet<THistogramStatePtr> GaugeHistograms_;
+ TCube<TGaugeHistogramSnapshot> GaugeHistogramsCube_;
+
+ THashSet<THistogramStatePtr> RateHistograms_;
+ TCube<TRateHistogramSnapshot> RateHistogramsCube_;
+
+ std::optional<ESensorType> Type_;
+ TGauge CubeSize_;
+ TGauge SensorsEmitted_;
+
+ void OnError(TError error);
+
+ void InitializeType(ESensorType type);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/tag_registry.cpp b/yt/yt/library/profiling/solomon/tag_registry.cpp
new file mode 100644
index 0000000000..f5413ab6df
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/tag_registry.cpp
@@ -0,0 +1,118 @@
+#include "tag_registry.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTagIdList TTagRegistry::Encode(const TTagList& tags)
+{
+ TTagIdList ids;
+
+ for (const auto& tag : tags) {
+ if (auto it = TagByName_.find(tag); it != TagByName_.end()) {
+ ids.push_back(it->second);
+ } else {
+ TagById_.push_back(tag);
+ TagByName_[tag] = TagById_.size();
+ ids.push_back(TagById_.size());
+ }
+ }
+
+ return ids;
+}
+
+TTagId TTagRegistry::Encode(const TTag& tag)
+{
+ if (auto it = TagByName_.find(tag); it != TagByName_.end()) {
+ return it->second;
+ } else {
+ TagById_.push_back(tag);
+ TagByName_[tag] = TagById_.size();
+ return TagById_.size();
+ }
+}
+
+TTagIdList TTagRegistry::Encode(const TTagSet& tags)
+{
+ return Encode(tags.Tags());
+}
+
+TCompactVector<std::optional<TTagId>, TypicalTagCount> TTagRegistry::TryEncode(const TTagList& tags) const
+{
+ TCompactVector<std::optional<TTagId>, TypicalTagCount> ids;
+
+ for (const auto& tag : tags) {
+ if (auto it = TagByName_.find(tag); it != TagByName_.end()) {
+ ids.push_back(it->second);
+ } else {
+ ids.push_back({});
+ }
+ }
+
+ return ids;
+}
+
+const TTag& TTagRegistry::Decode(TTagId tagId) const
+{
+ if (tagId < 1 || static_cast<size_t>(tagId) > TagById_.size()) {
+ THROW_ERROR_EXCEPTION("Invalid tag")
+ << TErrorAttribute("tag_id", tagId);
+ }
+
+ return TagById_[tagId - 1];
+}
+
+int TTagRegistry::GetSize() const
+{
+ return TagById_.size();
+}
+
+THashMap<TString, int> TTagRegistry::GetTopByKey() const
+{
+ THashMap<TString, int> counts;
+ for (const auto& [key, value] : TagById_) {
+ counts[key]++;
+ }
+ return counts;
+}
+
+void TTagRegistry::DumpTags(NProto::TSensorDump* dump)
+{
+ dump->add_tags();
+
+ for (int i = 0; i < std::ssize(TagById_); i++) {
+ auto tag = dump->add_tags();
+ tag->set_key(TagById_[i].first);
+ tag->set_value(TagById_[i].second);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTagWriter::WriteLabel(TTagId tag)
+{
+ if (static_cast<size_t>(tag) >= Cache_.size()) {
+ Cache_.resize(tag + 1);
+ }
+
+ auto& translation = Cache_[tag];
+ if (!translation) {
+ const auto& tagStr = Registry_.Decode(tag);
+ translation = Encoder_->PrepareLabel(tagStr.first, tagStr.second);
+ }
+
+ Encoder_->OnLabel(translation->first, translation->second);
+}
+
+const TTag& TTagWriter::Decode(TTagId tagId) const
+{
+ return Registry_.Decode(tagId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/tag_registry.h b/yt/yt/library/profiling/solomon/tag_registry.h
new file mode 100644
index 0000000000..22cbdc64e0
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/tag_registry.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/library/profiling/tag.h>
+#include <yt/yt/library/profiling/solomon/sensor_dump.pb.h>
+
+#include <library/cpp/monlib/encode/buffered/buffered_encoder_base.h>
+
+#include <util/generic/hash_set.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTagRegistry
+{
+public:
+ TTagIdList Encode(const TTagSet& tags);
+ TTagIdList Encode(const TTagList& tags);
+ TTagId Encode(const TTag& tag);
+
+ //! TryEncode returns null for an unknown tag.
+ TCompactVector<std::optional<TTagId>, TypicalTagCount> TryEncode(const TTagList& tags) const;
+
+ const TTag& Decode(TTagId tagId) const;
+ int GetSize() const;
+ THashMap<TString, int> GetTopByKey() const;
+
+ void DumpTags(NProto::TSensorDump* dump);
+
+private:
+ // TODO(prime@): maybe do something about the fact that tags are never freed.
+ THashMap<TTag, TTagId> TagByName_;
+ std::deque<TTag> TagById_;
+
+ THashMap<TTagId, TTagId> LegacyTags_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTagWriter
+{
+public:
+ TTagWriter(const TTagRegistry& registry, ::NMonitoring::IMetricConsumer* encoder)
+ : Registry_(registry)
+ , Encoder_(encoder)
+ { }
+
+ void WriteLabel(TTagId tag);
+ const TTag& Decode(TTagId tagId) const;
+
+private:
+ const TTagRegistry& Registry_;
+ ::NMonitoring::IMetricConsumer* Encoder_;
+
+ std::deque<std::optional<std::pair<ui32, ui32>>> Cache_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/solomon/ya.make b/yt/yt/library/profiling/solomon/ya.make
new file mode 100644
index 0000000000..4b20516ed2
--- /dev/null
+++ b/yt/yt/library/profiling/solomon/ya.make
@@ -0,0 +1,33 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ cube.cpp
+ exporter.cpp
+ percpu.cpp
+ producer.cpp
+ registry.cpp
+ remote.cpp
+ sensor.cpp
+ sensor_service.cpp
+ sensor_set.cpp
+ tag_registry.cpp
+
+ sensor_dump.proto
+)
+
+PEERDIR(
+ yt/yt/library/profiling
+ yt/yt/core
+ yt/yt/core/http
+
+ library/cpp/cgiparam
+ library/cpp/monlib/metrics
+ library/cpp/monlib/encode/prometheus
+ library/cpp/monlib/encode/spack
+ library/cpp/monlib/encode/json
+ library/cpp/yt/threading
+)
+
+END()
diff --git a/yt/yt/library/profiling/summary-inl.h b/yt/yt/library/profiling/summary-inl.h
new file mode 100644
index 0000000000..1402f1ff47
--- /dev/null
+++ b/yt/yt/library/profiling/summary-inl.h
@@ -0,0 +1,108 @@
+#ifndef SUMMARY_INL_H_
+#error "Direct inclusion of this file is not allowed, include summary.h"
+// For the sake of sane code completion.
+#include "summary.h"
+#endif
+#undef SUMMARY_INL_H_
+
+#include <util/generic/utility.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TSummarySnapshot<T>::TSummarySnapshot(T sum, T min, T max, T last, i64 count)
+ : Sum_(sum)
+ , Min_(min)
+ , Max_(max)
+ , Last_(last)
+ , Count_(count)
+{ }
+
+template <class T>
+void TSummarySnapshot<T>::Record(T value)
+{
+ if (Count_ == 0) {
+ Sum_ = Min_ = Max_ = value;
+ } else {
+ Sum_ += value;
+ Min_ = ::Min(Min_, value);
+ Max_ = ::Max(Max_, value);
+ }
+ Count_++;
+}
+
+template <class T>
+void TSummarySnapshot<T>::Add(const TSummarySnapshot& other)
+{
+ if (other.Count_ == 0) {
+ return;
+ }
+
+ if (Count_ == 0) {
+ *this = other;
+ } else {
+ Sum_ += other.Sum_;
+ Min_ = ::Min(Min_, other.Min_);
+ Max_ = ::Max(Max_, other.Max_);
+ Count_ += other.Count_;
+ }
+}
+
+template <class T>
+TSummarySnapshot<T>& TSummarySnapshot<T>::operator += (const TSummarySnapshot<T>& other)
+{
+ Add(other);
+ return *this;
+}
+
+template <class T>
+bool TSummarySnapshot<T>::operator == (const TSummarySnapshot<T>& other) const
+{
+ return Sum_ == other.Sum_ &&
+ Min_ == other.Min_ &&
+ Max_ == other.Max_ &&
+ Last_ == other.Last_ &&
+ Count_ == other.Count_;
+}
+
+template <class T>
+bool TSummarySnapshot<T>::operator != (const TSummarySnapshot<T>& other) const
+{
+ return !(*this == other);
+}
+
+template <class T>
+T TSummarySnapshot<T>::Sum() const
+{
+ return Sum_;
+}
+
+template <class T>
+T TSummarySnapshot<T>::Min() const
+{
+ return Min_;
+}
+
+template <class T>
+T TSummarySnapshot<T>::Max() const
+{
+ return Max_;
+}
+
+template <class T>
+T TSummarySnapshot<T>::Last() const
+{
+ return Last_;
+}
+
+template <class T>
+i64 TSummarySnapshot<T>::Count() const
+{
+ return Count_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/summary.h b/yt/yt/library/profiling/summary.h
new file mode 100644
index 0000000000..29768d1e2b
--- /dev/null
+++ b/yt/yt/library/profiling/summary.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <util/system/types.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TSummarySnapshot
+{
+public:
+ void Record(T value);
+ void Add(const TSummarySnapshot& other);
+
+ TSummarySnapshot<T>& operator += (const TSummarySnapshot& other);
+ TSummarySnapshot() = default;
+ TSummarySnapshot(T sum, T min, T max, T last, i64 count);
+
+ bool operator == (const TSummarySnapshot& other) const;
+ bool operator != (const TSummarySnapshot& other) const;
+
+ T Sum() const;
+ T Min() const;
+ T Max() const;
+ T Last() const;
+ i64 Count() const;
+
+private:
+ T Sum_{}, Min_{}, Max_{}, Last_{};
+ i64 Count_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
+
+#define SUMMARY_INL_H_
+#include "summary-inl.h"
+#undef SUMMARY_INL_H_
diff --git a/yt/yt/library/profiling/tag-inl.h b/yt/yt/library/profiling/tag-inl.h
new file mode 100644
index 0000000000..25bcd4d644
--- /dev/null
+++ b/yt/yt/library/profiling/tag-inl.h
@@ -0,0 +1,138 @@
+#ifndef TAG_INL_H_
+#error "Direct inclusion of this file is not allowed, include tag.h"
+// For the sake of sane code completion.
+#include "tag.h"
+#endif
+#undef TAG_INL_H_
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const TTagIndexList& TProjectionSet::Parents() const
+{
+ return Parents_;
+}
+
+inline const TTagIndexList& TProjectionSet::Children() const
+{
+ return Children_;
+}
+
+inline const TTagIndexList& TProjectionSet::Required() const
+{
+ return Required_;
+}
+
+inline const TTagIndexList& TProjectionSet::Excluded() const
+{
+ return Excluded_;
+}
+
+inline const TTagIndexList& TProjectionSet::Alternative() const
+{
+ return Alternative_;
+}
+
+inline const std::vector<std::pair<TDynamicTagPtr, TTagIndex>>& TProjectionSet::DynamicTags() const
+{
+ return DynamicTags_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TTagSet::TTagSet(const TTagList& tags)
+ : Tags_(tags)
+{
+ Resize(tags.size());
+}
+
+inline const TTagList& TTagSet::Tags() const
+{
+ return Tags_;
+}
+
+template <class TFn>
+void TProjectionSet::Range(
+ const TTagIdList& tags,
+ TFn fn) const
+{
+ if (Enabled_) {
+ RangeSubsets(tags, Parents_, Children_, Required_, Excluded_, Alternative_, fn);
+ } else {
+ fn(tags);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TFn>
+void RangeSubsets(
+ const TTagIdList& tags,
+ const TTagIndexList& parents,
+ const TTagIndexList& children,
+ const TTagIndexList& required,
+ const TTagIndexList& excluded,
+ const TTagIndexList& alternative,
+ TFn fn)
+{
+ auto toMask = [] (auto list) {
+ ui64 mask = 0;
+ for (auto i : list) {
+ mask |= 1 << i;
+ }
+ return mask;
+ };
+
+ ui64 requiredMask = toMask(required);
+ ui64 excludedMask = toMask(excluded);
+ YT_VERIFY(parents.size() == tags.size());
+
+ for (ui64 mask = 0; mask < (1 << tags.size()); ++mask) {
+ if ((mask & requiredMask) != requiredMask) {
+ continue;
+ }
+
+ if ((mask & excludedMask) != 0) {
+ continue;
+ }
+
+ bool skip = false;
+ for (size_t i = 0; i < tags.size(); i++) {
+ if (!(mask & (1 << i))) {
+ if (children[i] != NoTagSentinel && (mask & (1 << children[i]))) {
+ skip = true;
+ break;
+ }
+
+ continue;
+ }
+
+ if (parents[i] != NoTagSentinel && !(mask & (1 << parents[i]))) {
+ skip = true;
+ break;
+ }
+
+ if (alternative[i] != NoTagSentinel && (mask & (1 << alternative[i]))) {
+ skip = true;
+ break;
+ }
+ }
+ if (skip) {
+ continue;
+ }
+
+ TTagIdList list;
+ for (size_t i = 0; i < tags.size(); i++) {
+ if (mask & (1 << i)) {
+ list.push_back(tags[i]);
+ }
+ }
+
+ fn(list);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/tag.cpp b/yt/yt/library/profiling/tag.cpp
new file mode 100644
index 0000000000..0614ba5062
--- /dev/null
+++ b/yt/yt/library/profiling/tag.cpp
@@ -0,0 +1,217 @@
+#include "tag.h"
+
+#include <library/cpp/yt/memory/new.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTagIdList operator + (const TTagIdList& a, const TTagIdList& b)
+{
+ auto result = a;
+ result += b;
+ return result;
+}
+
+TTagIdList& operator += (TTagIdList& a, const TTagIdList& b)
+{
+ a.insert(a.end(), b.begin(), b.end());
+ return a;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TProjectionSet::Resize(int size)
+{
+ Parents_.resize(size, NoTagSentinel);
+ Children_.resize(size, NoTagSentinel);
+ Alternative_.resize(size, NoTagSentinel);
+}
+
+void TProjectionSet::SetEnabled(bool enabled)
+{
+ Enabled_ = enabled;
+}
+
+void TTagSet::Append(const TTagSet& other)
+{
+ auto offset = Tags_.size();
+
+ for (const auto& tag : other.Tags_) {
+ Tags_.push_back(tag);
+ }
+
+ for (auto i : other.Required_) {
+ Required_.push_back(offset + i);
+ }
+
+ for (auto i : other.Excluded_) {
+ Excluded_.push_back(offset + i);
+ }
+
+ for (auto i : other.Parents_) {
+ if (i == NoTagSentinel) {
+ Parents_.push_back(NoTagSentinel);
+ } else {
+ Parents_.push_back(i + offset);
+ }
+ }
+
+ for (auto i : other.Children_) {
+ if (i == NoTagSentinel) {
+ Children_.push_back(NoTagSentinel);
+ } else {
+ Children_.push_back(i + offset);
+ }
+ }
+
+ for (auto i : other.Alternative_) {
+ if (i == NoTagSentinel) {
+ Alternative_.push_back(NoTagSentinel);
+ } else {
+ Alternative_.push_back(i + offset);
+ }
+ }
+
+ for (auto [tag, index] : other.DynamicTags_) {
+ DynamicTags_.emplace_back(tag, index + offset);
+ }
+}
+
+TTagSet TTagSet::WithTag(TTag tag, int parent) const
+{
+ auto copy = *this;
+ copy.AddTag(std::move(tag), parent);
+ return copy;
+}
+
+TTagSet TTagSet::WithRequiredTag(TTag tag, int parent) const
+{
+ auto copy = *this;
+ copy.AddRequiredTag(std::move(tag), parent);
+ return copy;
+}
+
+TTagSet TTagSet::WithExcludedTag(TTag tag, int parent) const
+{
+ auto copy = *this;
+ copy.AddExcludedTag(std::move(tag), parent);
+ return copy;
+}
+
+TTagSet TTagSet::WithAlternativeTag(TTag tag, int alternativeTo, int parent) const
+{
+ auto copy = *this;
+ copy.AddAlternativeTag(std::move(tag), alternativeTo, parent);
+ return copy;
+}
+
+TTagSet TTagSet::WithExtensionTag(TTag tag, int extensionOf) const
+{
+ auto copy = *this;
+ copy.AddExtensionTag(std::move(tag), extensionOf);
+ return copy;
+}
+
+TTagSet TTagSet::WithTagWithChild(TTag tag, int child) const
+{
+ auto copy = *this;
+ copy.AddTagWithChild(std::move(tag), child);
+ return copy;
+}
+
+TTagSet TTagSet::WithTagSet(const TTagSet& other) const
+{
+ auto copy = *this;
+ copy.Append(other);
+ return copy;
+}
+
+void TTagSet::AddTag(TTag tag, int parent)
+{
+ int parentIndex = Tags_.size() + parent;
+ if (parentIndex >= 0 && static_cast<size_t>(parentIndex) < Tags_.size()) {
+ Parents_.push_back(parentIndex);
+ } else {
+ Parents_.push_back(NoTagSentinel);
+ }
+
+ Children_.push_back(NoTagSentinel);
+ Alternative_.push_back(NoTagSentinel);
+ Tags_.emplace_back(std::move(tag));
+}
+
+void TTagSet::AddRequiredTag(TTag tag, int parent)
+{
+ Required_.push_back(Tags_.size());
+ AddTag(std::move(tag), parent);
+}
+
+void TTagSet::AddExcludedTag(TTag tag, int parent)
+{
+ Excluded_.push_back(Tags_.size());
+ AddTag(std::move(tag), parent);
+}
+
+void TTagSet::AddAlternativeTag(TTag tag, int alternativeTo, int parent)
+{
+ int alternativeIndex = Tags_.size() + alternativeTo;
+
+ AddTag(std::move(tag), parent);
+
+ if (alternativeIndex >= 0 && static_cast<size_t>(alternativeIndex) < Tags_.size()) {
+ Alternative_.back() = alternativeIndex;
+ }
+}
+
+void TTagSet::AddExtensionTag(TTag tag, int extensionOf)
+{
+ int extensionIndex = Tags_.size() + extensionOf;
+ AddTag(std::move(tag), extensionOf);
+ Children_.back() = extensionIndex;
+}
+
+void TTagSet::AddTagWithChild(TTag tag, int child)
+{
+ int childIndex = Tags_.size() + child;
+ AddTag(tag);
+ Children_.back() = childIndex;
+}
+
+TDynamicTagPtr TTagSet::AddDynamicTag(int index)
+{
+ auto tag = New<TDynamicTag>();
+ DynamicTags_.emplace_back(tag, index);
+ return tag;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
+
+size_t THash<NYT::NProfiling::TTagIndexList>::operator()(const NYT::NProfiling::TTagIndexList& list) const
+{
+ size_t result = 0;
+ for (auto index : list) {
+ result = CombineHashes(result, std::hash<NYT::NProfiling::TTagIndex>()(index));
+ }
+ return result;
+}
+
+size_t THash<NYT::NProfiling::TTagList>::operator()(const NYT::NProfiling::TTagList& list) const
+{
+ size_t result = 0;
+ for (const auto& tag : list) {
+ result = CombineHashes(result, THash<NYT::NProfiling::TTag>()(tag));
+ }
+ return result;
+}
+
+size_t THash<NYT::NProfiling::TTagIdList>::operator()(const NYT::NProfiling::TTagIdList& list) const
+{
+ size_t result = 1;
+ for (auto tag : list) {
+ result = CombineHashes(result, std::hash<NYT::NProfiling::TTagId>()(tag));
+ }
+ return result;
+}
diff --git a/yt/yt/library/profiling/tag.h b/yt/yt/library/profiling/tag.h
new file mode 100644
index 0000000000..322b51b06d
--- /dev/null
+++ b/yt/yt/library/profiling/tag.h
@@ -0,0 +1,141 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/string.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/yt/memory/intrusive_ptr.h>
+
+#include <vector>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TTagId = int;
+
+constexpr int TypicalTagCount = 6;
+
+using TTagIdList = TCompactVector<TTagId, TypicalTagCount>;
+
+using TTag = std::pair<TString, TString>;
+
+using TTagList = TCompactVector<TTag, TypicalTagCount>;
+
+using TTagIndex = ui8;
+
+using TTagIndexList = TCompactVector<TTagIndex, TypicalTagCount>;
+
+constexpr ui8 NoTagSentinel = 0xff;
+
+constexpr int NoParent = 0;
+
+struct TDynamicTag final
+{ };
+
+using TDynamicTagPtr = TIntrusivePtr<TDynamicTag>;
+
+class TProjectionSet
+{
+public:
+ const TTagIndexList& Parents() const;
+ const TTagIndexList& Children() const;
+ const TTagIndexList& Required() const;
+ const TTagIndexList& Excluded() const;
+ const TTagIndexList& Alternative() const;
+
+ const std::vector<std::pair<TDynamicTagPtr, TTagIndex>>& DynamicTags() const;
+
+ template <class TFn>
+ void Range(
+ const TTagIdList& tags,
+ TFn fn) const;
+
+ void Resize(int size);
+ void SetEnabled(bool enabled);
+
+protected:
+ bool Enabled_ = true;
+ TTagIndexList Parents_;
+ TTagIndexList Children_;
+ TTagIndexList Required_;
+ TTagIndexList Excluded_;
+ TTagIndexList Alternative_;
+
+ std::vector<std::pair<TDynamicTagPtr, TTagIndex>> DynamicTags_;
+};
+
+class TTagSet
+ : public TProjectionSet
+{
+public:
+ TTagSet() = default;
+ explicit TTagSet(const TTagList& tags);
+
+ TTagSet WithTag(TTag tag, int parent = NoParent) const;
+ TTagSet WithRequiredTag(TTag tag, int parent = NoParent) const;
+ TTagSet WithExcludedTag(TTag tag, int parent = NoParent) const;
+ TTagSet WithAlternativeTag(TTag tag, int alternativeTo, int parent = NoParent) const;
+ TTagSet WithExtensionTag(TTag tag, int extensionOf) const;
+ TTagSet WithTagWithChild(TTag tag, int child) const;
+ TTagSet WithTagSet(const TTagSet& other) const;
+
+ void AddTag(TTag tag, int parent = NoParent);
+ void AddRequiredTag(TTag tag, int parent = NoParent);
+ void AddExcludedTag(TTag tag, int parent = NoParent);
+ void AddAlternativeTag(TTag tag, int alternativeTo, int parent = NoParent);
+ void AddExtensionTag(TTag tag, int extensionOf);
+ void AddTagWithChild(TTag tag, int child);
+ void Append(const TTagSet& other);
+
+ TDynamicTagPtr AddDynamicTag(int index);
+
+ const TTagList& Tags() const;
+
+private:
+ TTagList Tags_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TFn>
+void RangeSubsets(
+ const TTagIdList& tags,
+ const TTagIndexList& parents,
+ const TTagIndexList& children,
+ const TTagIndexList& required,
+ const TTagIndexList& excluded,
+ const TTagIndexList& alternative,
+ TFn fn);
+
+TTagIdList operator + (const TTagIdList& a, const TTagIdList& b);
+TTagIdList& operator += (TTagIdList& a, const TTagIdList& b);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
+
+template <>
+struct THash<NYT::NProfiling::TTagIndexList>
+{
+ size_t operator()(const NYT::NProfiling::TTagIndexList& ids) const;
+};
+
+template <>
+struct THash<NYT::NProfiling::TTagList>
+{
+ size_t operator()(const NYT::NProfiling::TTagList& ids) const;
+};
+
+template <>
+struct THash<NYT::NProfiling::TTagIdList>
+{
+ size_t operator()(const NYT::NProfiling::TTagIdList& ids) const;
+};
+
+#define TAG_INL_H_
+#include "tag-inl.h"
+#undef TAG_INL_H_
+
diff --git a/yt/yt/library/profiling/tcmalloc/profiler.cpp b/yt/yt/library/profiling/tcmalloc/profiler.cpp
new file mode 100644
index 0000000000..621ae9c1e4
--- /dev/null
+++ b/yt/yt/library/profiling/tcmalloc/profiler.cpp
@@ -0,0 +1,64 @@
+#include "profiler.h"
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/library/profiling/producer.h>
+
+#include <tcmalloc/malloc_extension.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProfilingStatisticsProducer
+ : public ISensorProducer
+{
+public:
+ TProfilingStatisticsProducer()
+ {
+ TProfiler profiler{""};
+ profiler.AddProducer("/memory", MakeStrong(this));
+ }
+
+ void CollectSensors(ISensorWriter* writer) override
+ {
+ for (const auto& property : TCMallocStats_) {
+ if (auto value = tcmalloc::MallocExtension::GetNumericProperty(property)) {
+ writer->AddGauge("/" + property, *value);
+ }
+ }
+ }
+
+private:
+ std::vector<TString> TCMallocStats_ = {
+ "tcmalloc.per_cpu_caches_active",
+ "generic.virtual_memory_used",
+ "generic.physical_memory_used",
+ "generic.bytes_in_use_by_app",
+ "generic.heap_size",
+ "tcmalloc.central_cache_free",
+ "tcmalloc.cpu_free",
+ "tcmalloc.page_heap_free",
+ "tcmalloc.page_heap_unmapped",
+ "tcmalloc.page_algorithm",
+ "tcmalloc.max_total_thread_cache_bytes",
+ "tcmalloc.thread_cache_free",
+ "tcmalloc.thread_cache_count",
+ "tcmalloc.local_bytes",
+ "tcmalloc.external_fragmentation_bytes",
+ "tcmalloc.metadata_bytes",
+ "tcmalloc.transfer_cache_free",
+ "tcmalloc.hard_usage_limit_bytes",
+ "tcmalloc.desired_usage_limit_bytes",
+ "tcmalloc.required_bytes"
+ };
+};
+
+void EnableTCMallocProfiler()
+{
+ LeakyRefCountedSingleton<TProfilingStatisticsProducer>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/tcmalloc/profiler.h b/yt/yt/library/profiling/tcmalloc/profiler.h
new file mode 100644
index 0000000000..8bf9237992
--- /dev/null
+++ b/yt/yt/library/profiling/tcmalloc/profiler.h
@@ -0,0 +1,11 @@
+#pragma once
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void EnableTCMallocProfiler();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/tcmalloc/ya.make b/yt/yt/library/profiling/tcmalloc/ya.make
new file mode 100644
index 0000000000..2ba6ed525d
--- /dev/null
+++ b/yt/yt/library/profiling/tcmalloc/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ profiler.cpp
+)
+
+PEERDIR(
+ yt/yt/library/profiling
+ contrib/libs/tcmalloc/malloc_extension
+)
+
+END()
diff --git a/yt/yt/library/profiling/testing.cpp b/yt/yt/library/profiling/testing.cpp
new file mode 100644
index 0000000000..22276ac006
--- /dev/null
+++ b/yt/yt/library/profiling/testing.cpp
@@ -0,0 +1,41 @@
+#include "testing.h"
+#include "impl.h"
+
+#include <util/generic/string.h>
+#include <util/generic/yexception.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+double TTesting::ReadGauge(const TGauge& gauge)
+{
+ Y_ENSURE(gauge.Gauge_, "Gauge is not registered");
+ return gauge.Gauge_->GetValue();
+}
+
+TDuration TTesting::ReadTimeGauge(const TTimeGauge& gauge)
+{
+ Y_ENSURE(gauge.Gauge_, "Gauge is not registered");
+ return gauge.Gauge_->GetValue();
+}
+
+i64 TTesting::ReadCounter(const TCounter& counter)
+{
+ Y_ENSURE(counter.Counter_, "Counter is not registered");
+ return counter.Counter_->GetValue();
+}
+
+TDuration TTesting::ReadTimeCounter(const TTimeCounter& counter)
+{
+ Y_ENSURE(counter.Counter_, "Counter is not registered");
+ return counter.Counter_->GetValue();
+}
+
+const TSensorOptions& TTesting::ReadOptions(const TProfiler& profiler) {
+ return profiler.Options_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/testing.h b/yt/yt/library/profiling/testing.h
new file mode 100644
index 0000000000..116706a951
--- /dev/null
+++ b/yt/yt/library/profiling/testing.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "sensor.h"
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTesting
+{
+ static double ReadGauge(const TGauge& gauge);
+ static TDuration ReadTimeGauge(const TTimeGauge& gauge);
+ static i64 ReadCounter(const TCounter& counter);
+ static TDuration ReadTimeCounter(const TTimeCounter& counter);
+
+ static const TSensorOptions& ReadOptions(const TProfiler& profiler);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/cube_ut.cpp b/yt/yt/library/profiling/unittests/cube_ut.cpp
new file mode 100644
index 0000000000..c687dde5d4
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/cube_ut.cpp
@@ -0,0 +1,65 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/profiling/solomon/cube.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TCube, GaugeProjections)
+{
+ TCube<double> cube(12, 0);
+
+ cube.StartIteration();
+ cube.Add({ 1 });
+ cube.Add({ 1 });
+ cube.Update({ 1 }, 1.0);
+ cube.Update({ 1 }, 3.0);
+ cube.FinishIteration();
+
+ const auto& p = cube.GetProjections();
+ auto it = p.find(TTagIdList{ 1 });
+ ASSERT_TRUE(it != p.end());
+
+ EXPECT_EQ(4.0, it->second.Values[0]);
+
+ cube.StartIteration();
+ cube.Remove({ 1 });
+ cube.FinishIteration();
+
+ ASSERT_EQ(static_cast<size_t>(1), cube.GetProjections().size());
+
+ cube.StartIteration();
+ cube.Remove({ 1 });
+ cube.FinishIteration();
+
+ ASSERT_EQ(static_cast<size_t>(0), cube.GetProjections().size());
+}
+
+TEST(TCube, Rollup)
+{
+ TCube<i64> cube(12, 0);
+ cube.StartIteration();
+ cube.Add({});
+ cube.Update({}, 1);
+ cube.FinishIteration();
+
+ for (int i = 0; i < 100; i++) {
+ cube.StartIteration();
+ if (i % 2 == 0) {
+ cube.Update({}, 1);
+ }
+ cube.FinishIteration();
+ }
+
+ auto it = cube.GetProjections().find(TTagIdList{});
+ ASSERT_TRUE(it != cube.GetProjections().end());
+
+ ASSERT_EQ(it->second.Rollup, static_cast<i64>(51 - 12 / 2));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/deps/main.cpp b/yt/yt/library/profiling/unittests/deps/main.cpp
new file mode 100644
index 0000000000..ac20c4df89
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/deps/main.cpp
@@ -0,0 +1,18 @@
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/producer.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYT::NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+int main() {
+ NYT::NProfiling::TProfiler profiler{"/foo"};
+ auto gauge = profiler.Gauge("/bar");
+ gauge.Update(1.0);
+
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/library/profiling/unittests/deps/ya.make b/yt/yt/library/profiling/unittests/deps/ya.make
new file mode 100644
index 0000000000..c19da9b3b4
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/deps/ya.make
@@ -0,0 +1,11 @@
+PROGRAM()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(main.cpp)
+
+PEERDIR(
+ yt/yt/library/profiling
+)
+
+END()
diff --git a/yt/yt/library/profiling/unittests/exporter_ut.cpp b/yt/yt/library/profiling/unittests/exporter_ut.cpp
new file mode 100644
index 0000000000..11c50734eb
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/exporter_ut.cpp
@@ -0,0 +1,222 @@
+#include "yt/yt/library/profiling/solomon/registry.h"
+#include <gtest/gtest.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+
+#include <yt/yt/library/profiling/solomon/exporter.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSolomonExporter, MemoryLeak)
+{
+ auto registry = New<TSolomonRegistry>();
+ auto counter = TProfiler{registry, "yt"}.Counter("/foo");
+
+ auto config = New<TSolomonExporterConfig>();
+ config->GridStep = TDuration::Seconds(1);
+ config->EnableCoreProfilingCompatibility = true;
+ config->EnableSelfProfiling = false;
+
+ auto exporter = New<TSolomonExporter>(config, registry);
+ auto json = exporter->ReadJson();
+ EXPECT_FALSE(json);
+ auto spack = exporter->ReadSpack();
+ EXPECT_FALSE(spack);
+
+ exporter->Start();
+
+ Sleep(TDuration::Seconds(5));
+
+ json = exporter->ReadJson();
+ EXPECT_TRUE(json);
+ EXPECT_FALSE(json->empty());
+ spack = exporter->ReadSpack();
+ EXPECT_TRUE(spack);
+ EXPECT_FALSE(spack->empty());
+
+ exporter->Stop();
+}
+
+TEST(TSolomonExporter, ReadJsonHistogram)
+{
+ auto registry = New<TSolomonRegistry>();
+ auto hist = TProfiler{registry, "yt"}.TimeHistogram("/foo", TDuration::MilliSeconds(1), TDuration::Seconds(1));
+
+ auto config = New<TSolomonExporterConfig>();
+ config->GridStep = TDuration::Seconds(1);
+ config->EnableCoreProfilingCompatibility = true;
+ config->EnableSelfProfiling = false;
+
+ auto exporter = NYT::New<TSolomonExporter>(config, registry);
+ auto json = exporter->ReadJson();
+ EXPECT_FALSE(json);
+
+ exporter->Start();
+
+ hist.Record(TDuration::MilliSeconds(500));
+ hist.Record(TDuration::MilliSeconds(500));
+ hist.Record(TDuration::MilliSeconds(500));
+ Sleep(TDuration::Seconds(5));
+
+ json = exporter->ReadJson();
+ ASSERT_TRUE(json);
+ Cerr << *json;
+
+ exporter->Stop();
+}
+
+TEST(TSolomonExporter, ReadSpackHistogram)
+{
+ auto registry = New<TSolomonRegistry>();
+ auto hist = TProfiler{registry, "yt"}.TimeHistogram("/foo", TDuration::MilliSeconds(1), TDuration::Seconds(1));
+
+ auto config = New<TSolomonExporterConfig>();
+ config->GridStep = TDuration::Seconds(1);
+ config->EnableCoreProfilingCompatibility = true;
+ config->EnableSelfProfiling = false;
+
+ auto exporter = NYT::New<TSolomonExporter>(config, registry);
+ auto spack = exporter->ReadSpack();
+ EXPECT_FALSE(spack);
+
+ exporter->Start();
+
+ hist.Record(TDuration::MilliSeconds(500));
+ hist.Record(TDuration::MilliSeconds(500));
+ hist.Record(TDuration::MilliSeconds(500));
+ Sleep(TDuration::Seconds(5));
+
+ spack = exporter->ReadSpack();
+ ASSERT_TRUE(spack);
+ Cerr << *spack;
+
+ exporter->Stop();
+}
+
+TEST(TSolomonExporter, ReadSensorsFilter)
+{
+ auto registry = New<TSolomonRegistry>();
+
+ THashMap<TString, NYT::NProfiling::TShardConfigPtr> shards;
+ auto AddShardConfig = [&shards] (TString shardName) -> void {
+ auto shardConfig = New<TShardConfig>();
+ shardConfig->GridStep = TDuration::Seconds(1);
+ shardConfig->Filter = {shardName};
+
+ shards.try_emplace(shardName, shardConfig);
+ };
+ AddShardConfig("/uptime/");
+ AddShardConfig("/cache/");
+ AddShardConfig("/requests/");
+
+ auto config = New<TSolomonExporterConfig>();
+ config->GridStep = TDuration::Seconds(1);
+ config->EnableCoreProfilingCompatibility = true;
+ config->EnableSelfProfiling = false;
+ config->Shards = std::move(shards);
+
+ auto exporter = NYT::New<TSolomonExporter>(config, registry);
+
+ TGauge uptime = TProfiler("/uptime/", "", {}, registry).Gauge("uptime");
+ TGauge cache_size = TProfiler("/cache/", "", {}, registry).Gauge("size");
+ TGauge responses = TProfiler("/requests/", "", {}, registry).Gauge("responses");
+
+ auto isSensorInShard = [&exporter] (const TString& shardName, const TString& sensor) -> bool {
+ std::optional<TString> out = exporter->ReadJson({}, shardName);
+ if (!out) {
+ return false;
+ }
+
+ const TString& sensors = out.value();
+ return sensors.Contains(sensor);
+ };
+
+ ASSERT_FALSE(isSensorInShard("/uptime/", "uptime"));
+ ASSERT_FALSE(isSensorInShard("/cache/", "size"));
+ ASSERT_FALSE(isSensorInShard("/requests/", "responses"));
+
+ exporter->Start();
+
+ uptime.Update(42);
+ cache_size.Update(69);
+
+ Sleep(TDuration::Seconds(5));
+
+ // uptime
+ ASSERT_TRUE(isSensorInShard("/uptime/", "uptime"));
+ ASSERT_FALSE(isSensorInShard("/uptime/", "size"));
+ ASSERT_FALSE(isSensorInShard("/uptime/", "responses"));
+
+ // cache
+ ASSERT_FALSE(isSensorInShard("/cache/", "uptime"));
+ ASSERT_TRUE(isSensorInShard("/cache/", "size"));
+ ASSERT_FALSE(isSensorInShard("/cache/", "responses"));
+
+ // requests
+ ASSERT_FALSE(isSensorInShard("/requests/", "uptime"));
+ ASSERT_FALSE(isSensorInShard("/requests/", "size"));
+ ASSERT_TRUE(isSensorInShard("/requests/", "responses"));
+
+ exporter->Stop();
+}
+
+TEST(TSolomonExporter, ReadSensorsStripSensorsOption)
+{
+ auto registry = New<TSolomonRegistry>();
+
+ THashMap<TString, NYT::NProfiling::TShardConfigPtr> shards;
+ auto AddShardConfig = [&shards] (TString shardName) -> void {
+ auto shardConfig = New<TShardConfig>();
+ shardConfig->GridStep = TDuration::Seconds(1);
+ shardConfig->Filter = {shardName};
+
+ shards.try_emplace(shardName, shardConfig);
+ };
+ AddShardConfig("/uptime/");
+
+ auto config = New<TSolomonExporterConfig>();
+ config->GridStep = TDuration::Seconds(1);
+ config->EnableCoreProfilingCompatibility = true;
+ config->EnableSelfProfiling = false;
+ config->Shards = std::move(shards);
+
+ auto exporter = NYT::New<TSolomonExporter>(config, registry);
+
+ TGauge uptime = TProfiler("/uptime/", "", {}, registry).Gauge("uptime");
+
+ exporter->Start();
+
+ uptime.Update(42);
+
+ Sleep(TDuration::Seconds(5));
+
+ // WO Strip option
+ std::optional<TString> out = exporter->ReadJson({}, "/uptime/");
+ ASSERT_TRUE(out);
+
+ TString& sensors = out.value();
+ ASSERT_TRUE(sensors.Contains("uptime"));
+ ASSERT_TRUE(sensors.Contains("uptime.")); // not "/uptime/" Reason: sensor rename
+
+ // With Strip option
+ TReadOptions options;
+ options.StripSensorsNamePrefix = true;
+ out = exporter->ReadJson(options, "/uptime/");
+ ASSERT_TRUE(out);
+
+ sensors = out.value();
+ ASSERT_TRUE(sensors.Contains("uptime"));
+ ASSERT_FALSE(sensors.Contains("uptime."));
+
+ exporter->Stop();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/name_conflicts_ut.cpp b/yt/yt/library/profiling/unittests/name_conflicts_ut.cpp
new file mode 100644
index 0000000000..d8e486b7ed
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/name_conflicts_ut.cpp
@@ -0,0 +1,6 @@
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/producer.h>
+#include <yt/yt/library/profiling/impl.h>
+
+#include <yt/yt/library/profiling/solomon/registry.h>
+#include <yt/yt/library/profiling/solomon/exporter.h> \ No newline at end of file
diff --git a/yt/yt/library/profiling/unittests/perf_counter_ut.cpp b/yt/yt/library/profiling/unittests/perf_counter_ut.cpp
new file mode 100644
index 0000000000..1482f97012
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/perf_counter_ut.cpp
@@ -0,0 +1,63 @@
+#include "util/system/yield.h"
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/profiling/perf/counters.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/proc.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TFn>
+void IgnorePermissionError(const TFn& fn)
+{
+ try {
+ fn();
+ } catch (const TErrorException& ex) {
+ constexpr auto PermissionErrorCode = TErrorCode(LinuxErrorCodeBase + EACCES);
+ if (ex.Error().FindMatching(PermissionErrorCode)) {
+ return;
+ }
+
+ throw;
+ }
+}
+
+TEST(TPerfCounters, Cycles)
+{
+ IgnorePermissionError([&] {
+ TPerfEventCounter counter(EPerfEventType::CpuCycles);
+ ASSERT_GE(counter.Read(), 0u);
+ });
+}
+
+TEST(TPerfCounters, ContextSwitches)
+{
+ IgnorePermissionError([&] {
+ TPerfEventCounter counter(EPerfEventType::ContextSwitches);
+
+ for (int i = 0; i < 10; i++) {
+ SchedYield();
+ }
+
+ ASSERT_GE(counter.Read(), 0u);
+ });
+}
+
+TEST(TPerfCounters, CounterError)
+{
+ auto createCounter = [] {
+ TPerfEventCounter counter{EPerfEventType::StalledCyclesBackend};
+ return 0;
+ };
+
+ ASSERT_THROW(createCounter(), TErrorException);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/profiler_ut.cpp b/yt/yt/library/profiling/unittests/profiler_ut.cpp
new file mode 100644
index 0000000000..c328b3f221
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/profiler_ut.cpp
@@ -0,0 +1,84 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/impl.h>
+#include <yt/yt/library/profiling/testing.h>
+
+#include <yt/yt/library/profiling/solomon/registry.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+TEST(Profiler, SaveOptions)
+{
+ auto profiler = TProfiler(
+ "my_prefix", "my_namespace", TTagSet{{{"my_tag", "my_tag_value"}}}, nullptr,
+ TSensorOptions{.TimeHistogramBounds = {TDuration::Seconds(1)}});
+
+ {
+ ASSERT_FALSE(TTesting::ReadOptions(profiler).Global);
+ auto newProfiler = profiler.WithGlobal();
+ ASSERT_TRUE(TTesting::ReadOptions(newProfiler).Global);
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ ASSERT_FALSE(TTesting::ReadOptions(profiler).Sparse);
+ auto newProfiler = profiler.WithSparse();
+ ASSERT_TRUE(TTesting::ReadOptions(newProfiler).Sparse);
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ ASSERT_FALSE(TTesting::ReadOptions(profiler).Hot);
+ auto newProfiler = profiler.WithHot();
+ ASSERT_TRUE(TTesting::ReadOptions(newProfiler).Hot);
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ ASSERT_FALSE(TTesting::ReadOptions(profiler).DisableDefault);
+ auto newProfiler = profiler.WithDefaultDisabled();
+ ASSERT_TRUE(TTesting::ReadOptions(newProfiler).DisableDefault);
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ ASSERT_FALSE(TTesting::ReadOptions(profiler).DisableProjections);
+ auto newProfiler = profiler.WithProjectionsDisabled();
+ ASSERT_TRUE(TTesting::ReadOptions(newProfiler).DisableProjections);
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ ASSERT_FALSE(TTesting::ReadOptions(profiler).DisableSensorsRename);
+ auto newProfiler = profiler.WithRenameDisabled();
+ ASSERT_TRUE(TTesting::ReadOptions(newProfiler).DisableSensorsRename);
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+
+ {
+ auto newProfiler = profiler.WithPrefix("new_prefix");
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ auto newProfiler = profiler.WithTag("new_key", "new_value");
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ auto newProfiler = profiler.WithRequiredTag("new_key", "new_value");
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ auto newProfiler = profiler.WithExcludedTag("new_key", "new_value");
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+ {
+ auto newProfiler = profiler.WithAlternativeTag("new_key", "new_value", 0);
+ ASSERT_EQ(TTesting::ReadOptions(newProfiler).TimeHistogramBounds.size(), 1u);
+ }
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/sensor_ut.cpp b/yt/yt/library/profiling/unittests/sensor_ut.cpp
new file mode 100644
index 0000000000..e640ace27c
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/sensor_ut.cpp
@@ -0,0 +1,31 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/impl.h>
+#include <yt/yt/library/profiling/testing.h>
+
+#include <yt/yt/library/profiling/solomon/registry.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProfiler EventQueue{"/event_queue"};
+
+auto GlobalEventCounter = EventQueue.Counter("/event_counter");
+auto GlobalQueueSize = EventQueue.Gauge("/queue_size");
+
+TEST(Sensor, TestingApi)
+{
+ GlobalEventCounter.Increment();
+ ASSERT_EQ(1, TTesting::ReadCounter(GlobalEventCounter));
+
+ GlobalQueueSize.Update(10);
+ ASSERT_EQ(10, TTesting::ReadGauge(GlobalQueueSize));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/solomon_ut.cpp b/yt/yt/library/profiling/unittests/solomon_ut.cpp
new file mode 100644
index 0000000000..708a8af93b
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/solomon_ut.cpp
@@ -0,0 +1,878 @@
+#include "yt/yt/core/misc/ref_counted.h"
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/impl.h>
+#include <yt/yt/library/profiling/producer.h>
+
+#include <yt/yt/library/profiling/solomon/registry.h>
+#include <yt/yt/library/profiling/solomon/remote.h>
+
+#include <util/string/join.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTestMetricConsumer
+ : public NMonitoring::IMetricConsumer
+{
+ void OnStreamBegin() override
+ { }
+
+ void OnStreamEnd() override
+ { }
+
+ void OnCommonTime(TInstant) override
+ { }
+
+ void OnMetricBegin(NMonitoring::EMetricType) override
+ { }
+
+ void OnMetricEnd() override
+ { }
+
+ void OnLabelsBegin() override
+ {
+ Labels.clear();
+ }
+
+ void OnLabelsEnd() override
+ { }
+
+ void OnLabel(TStringBuf name, TStringBuf value) override
+ {
+ if (name == "sensor") {
+ Name = value;
+ } else {
+ Labels.emplace_back(TString(name) + "=" + value);
+ }
+ }
+
+ void OnLabel(ui32 name, ui32 value) override
+ {
+ OnLabel(LabelsCache[name], LabelsCache[value]);
+ }
+
+ std::pair<ui32, ui32> PrepareLabel(const TStringBuf name, const TStringBuf value) override
+ {
+ LabelsCache.emplace_back(name);
+ LabelsCache.emplace_back(value);
+ return {LabelsCache.size() - 2, LabelsCache.size() - 1};
+ }
+
+ void OnDouble(TInstant, double value) override
+ {
+ Cerr << FormatName() << " " << value << Endl;
+ Gauges[FormatName()] = value;
+ }
+
+ void OnUint64(TInstant, ui64) override
+ { }
+
+ void OnInt64(TInstant, i64 value) override
+ {
+ Cerr << FormatName() << " " << value << Endl;
+ Counters[FormatName()] = value;
+ }
+
+ void OnHistogram(TInstant, NMonitoring::IHistogramSnapshotPtr value) override
+ {
+ Cerr << FormatName() << " histogram{";
+ for (size_t i = 0; i < value->Count(); ++i) {
+ Cerr << value->UpperBound(i) << ":" << value->Value(i);
+ if (i + 1 != value->Count()) {
+ Cerr << ", ";
+ }
+ }
+ Cerr << "}" << Endl;
+ Histograms[FormatName()] = value;
+ }
+
+ void OnLogHistogram(TInstant, NMonitoring::TLogHistogramSnapshotPtr) override
+ { }
+
+ void OnSummaryDouble(TInstant, NMonitoring::ISummaryDoubleSnapshotPtr snapshot) override
+ {
+ Cerr << FormatName() << " summary{"
+ << "min: " << snapshot->GetMin()
+ << ", max: " << snapshot->GetMax()
+ << ", sum: " << snapshot->GetSum()
+ << ", count: " << snapshot->GetCount()
+ << ", last: " << snapshot->GetLast()
+ << "}" << Endl;
+ }
+
+ TString Name;
+ std::vector<TString> Labels;
+
+ THashMap<TString, i64> Counters;
+ THashMap<TString, double> Gauges;
+ THashMap<TString, NMonitoring::IHistogramSnapshotPtr> Histograms;
+
+ std::vector<TString> LabelsCache;
+
+ TString FormatName() const
+ {
+ return Name + "{" + JoinSeq(";", Labels) + "}";
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSolomonRegistry, Registration)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/debug");
+
+ auto counter = profiler.Counter("/c0");
+ auto gauge = profiler.Gauge("/g0");
+
+ impl->ProcessRegistrations();
+
+ counter.Increment(1);
+ gauge.Update(42);
+}
+
+TTestMetricConsumer CollectSensors(TSolomonRegistryPtr impl, int subsample = 1, bool enableHack = false)
+{
+ impl->ProcessRegistrations();
+
+ auto i = impl->GetNextIteration();
+ impl->Collect();
+
+ TTestMetricConsumer testConsumer;
+
+ TReadOptions options;
+ options.EnableSolomonAggregationWorkaround = enableHack;
+ options.Times = {{{}, TInstant::Now()}};
+ for (int j = subsample - 1; j >= 0; --j) {
+ options.Times[0].first.push_back(impl->IndexOf(i - j));
+ }
+
+ impl->ReadSensors(options, &testConsumer);
+ Cerr << "-------------------------------------" << Endl;
+
+ return testConsumer;
+}
+
+TTestMetricConsumer ReadSensors(TSolomonRegistryPtr impl)
+{
+ auto i = impl->GetNextIteration();
+
+ TTestMetricConsumer testConsumer;
+
+ TReadOptions options;
+ options.Times = {{{impl->IndexOf(i - 1)}, TInstant::Now()}};
+
+ impl->ReadSensors(options, &testConsumer);
+ Cerr << "-------------------------------------" << Endl;
+
+ return testConsumer;
+}
+
+TEST(TSolomonRegistry, CounterProjections)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto c0 = profiler.WithTag("user", "u0").Counter("/count");
+ auto c1 = profiler.WithTag("user", "u1").Counter("/count");
+
+ auto result = CollectSensors(impl).Counters;
+
+ ASSERT_EQ(result["yt.d.count{}"], 0u);
+ ASSERT_EQ(result["yt.d.count{user=u0}"], 0u);
+
+ c0.Increment();
+ c1.Increment();
+
+ result = CollectSensors(impl).Counters;
+
+ ASSERT_EQ(result["yt.d.count{}"], 2u);
+ ASSERT_EQ(result["yt.d.count{user=u0}"], 1u);
+
+ c0.Increment();
+ c1 = {};
+
+ result = CollectSensors(impl).Counters;
+ ASSERT_EQ(result["yt.d.count{}"], 3u);
+ ASSERT_EQ(result["yt.d.count{user=u0}"], 2u);
+ ASSERT_EQ(result.find("yt.d.count{user=u1}"), result.end());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, GaugeProjections)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto g0 = profiler.WithTag("user", "u0").Gauge("/memory");
+ auto g1 = profiler.WithTag("user", "u1").Gauge("/memory");
+
+ auto result = CollectSensors(impl).Gauges;
+
+ ASSERT_EQ(result["yt.d.memory{}"], 0.0);
+ ASSERT_EQ(result["yt.d.memory{user=u0}"], 0.0);
+
+ g0.Update(1.0);
+ g1.Update(2.0);
+
+ result = CollectSensors(impl).Gauges;
+ ASSERT_EQ(result["yt.d.memory{}"], 3.0);
+ ASSERT_EQ(result["yt.d.memory{user=u0}"], 1.0);
+
+ g0.Update(10.0);
+ g1 = {};
+
+ result = CollectSensors(impl).Gauges;
+ ASSERT_EQ(result["yt.d.memory{}"], 10.0);
+ ASSERT_EQ(result["yt.d.memory{user=u0}"], 10.0);
+ ASSERT_EQ(result.find("yt.d.memory{user=u1}"), result.end());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, ExponentialHistogramProjections)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto c0 = profiler.WithTag("user", "u0").TimeHistogram("/histogram", TDuration::Zero(), TDuration::MilliSeconds(20));
+ auto c1 = profiler.WithTag("user", "u1").TimeHistogram("/histogram", TDuration::Zero(), TDuration::MilliSeconds(20));
+
+ auto result = CollectSensors(impl).Histograms;
+
+ ASSERT_EQ(result["yt.d.histogram{}"]->Count(), 16u);
+ ASSERT_EQ(result["yt.d.histogram{user=u0}"]->Count(), 16u);
+
+ c0.Record(TDuration::MilliSeconds(5));
+ c1.Record(TDuration::MilliSeconds(5));
+ c0.Record(TDuration::MilliSeconds(30));
+
+ result = CollectSensors(impl).Histograms;
+
+ ASSERT_EQ(result["yt.d.histogram{}"]->Count(), 16u);
+ ASSERT_EQ(result["yt.d.histogram{}"]->Value(13), 2u);
+ ASSERT_EQ(result["yt.d.histogram{user=u0}"]->Value(13), 1u);
+
+ ASSERT_EQ(result["yt.d.histogram{}"]->Value(15), 1u);
+ ASSERT_EQ(Max<double>(), result["yt.d.histogram{}"]->UpperBound(15));
+
+ c0.Record(TDuration::MilliSeconds(10));
+ c1 = {};
+
+ result = CollectSensors(impl).Histograms;
+ ASSERT_EQ(result["yt.d.histogram{}"]->Value(14), 1u);
+ ASSERT_EQ(result["yt.d.histogram{user=u0}"]->Value(14), 1u);
+ ASSERT_EQ(result.find("yt.d.histogram{user=u1}"), result.end());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, DifferentBuckets)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ std::vector<TDuration> firstBounds{
+ TDuration::Zero(), TDuration::MilliSeconds(5), TDuration::MilliSeconds(10)
+ };
+
+ std::vector<TDuration> secondBounds{
+ TDuration::Zero(), TDuration::MilliSeconds(500), TDuration::MilliSeconds(1000)
+ };
+
+ auto c0 = profiler.WithTag("user", "u0").TimeHistogram("/histogram", firstBounds);
+ auto c1 = profiler.WithTag("user", "u1").TimeHistogram("/histogram", secondBounds);
+
+ auto result = CollectSensors(impl).Histograms;
+
+ ASSERT_EQ(result.size(), 3u);
+ ASSERT_EQ(result["yt.d.histogram{}"]->Count(), 6u);
+ ASSERT_EQ(result["yt.d.histogram{user=u0}"]->Count(), 4u);
+}
+
+TEST(TSolomonRegistry, CustomHistogramProjections)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ std::vector<TDuration> bounds{
+ TDuration::Zero(), TDuration::MilliSeconds(5), TDuration::MilliSeconds(10), TDuration::MilliSeconds(15)
+ };
+ auto c0 = profiler.WithTag("user", "u0").TimeHistogram("/histogram", bounds);
+ auto c1 = profiler.WithTag("user", "u1").TimeHistogram("/histogram", bounds);
+
+ auto result = CollectSensors(impl).Histograms;
+
+ ASSERT_EQ(result["yt.d.histogram{}"]->Count(), 5u);
+ ASSERT_EQ(result["yt.d.histogram{user=u0}"]->Count(), 5u);
+
+ c0.Record(TDuration::MilliSeconds(5));
+ c1.Record(TDuration::MilliSeconds(5));
+ c0.Record(TDuration::MilliSeconds(16));
+
+ result = CollectSensors(impl).Histograms;
+
+ ASSERT_EQ(result["yt.d.histogram{}"]->Count(), 5u);
+ ASSERT_EQ(result["yt.d.histogram{}"]->Value(1), 2u);
+ ASSERT_EQ(result["yt.d.histogram{user=u0}"]->Value(1), 1u);
+
+ ASSERT_EQ(result["yt.d.histogram{}"]->Value(4), 1u);
+ ASSERT_EQ(Max<double>(), result["yt.d.histogram{}"]->UpperBound(4));
+
+ c0.Record(TDuration::MilliSeconds(10));
+ c1 = {};
+
+ result = CollectSensors(impl).Histograms;
+ ASSERT_EQ(result["yt.d.histogram{}"]->Value(2), 1u);
+ ASSERT_EQ(result["yt.d.histogram{user=u0}"]->Value(2), 1u);
+ ASSERT_EQ(result.find("yt.d.histogram{user=u1}"), result.end());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, SparseHistogram)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto h0 = profiler.WithSparse().TimeHistogram("/histogram", TDuration::Zero(), TDuration::MilliSeconds(20));
+
+ auto result = CollectSensors(impl).Histograms;
+ ASSERT_TRUE(result.empty());
+
+ h0.Record(TDuration::MilliSeconds(5));
+ result = CollectSensors(impl).Histograms;
+
+ ASSERT_FALSE(result.empty());
+ ASSERT_EQ(result["yt.d.histogram{}"]->Count(), 16u);
+ ASSERT_EQ(result["yt.d.histogram{}"]->Value(13), 1u);
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, SparseCounters)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto c = profiler.WithSparse().Counter("/sparse_counter");
+
+ auto result = CollectSensors(impl).Counters;
+ ASSERT_TRUE(result.empty());
+
+ c.Increment();
+ result = CollectSensors(impl).Counters;
+ ASSERT_EQ(result["yt.d.sparse_counter{}"], 1u);
+
+ result = CollectSensors(impl).Counters;
+ ASSERT_TRUE(result.empty());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+
+ c.Increment();
+ result = CollectSensors(impl).Counters;
+ ASSERT_EQ(result["yt.d.sparse_counter{}"], 2u);
+}
+
+TEST(TSolomonRegistry, GaugesNoDefault)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto g = profiler.WithDefaultDisabled().Gauge("/gauge");
+
+ auto result = CollectSensors(impl).Gauges;
+ ASSERT_TRUE(result.empty());
+
+ g.Update(1);
+ result = CollectSensors(impl).Gauges;
+ ASSERT_EQ(result["yt.d.gauge{}"], 1.0);
+}
+
+TEST(TSolomonRegistry, SparseCountersWithHack)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto c = profiler.WithSparse().Counter("/sparse_counter_with_hack");
+
+ auto result = CollectSensors(impl, 1, true).Counters;
+ ASSERT_TRUE(result.empty());
+
+ c.Increment();
+ result = CollectSensors(impl, 1, true).Counters;
+ ASSERT_EQ(result["yt.d.sparse_counter_with_hack{}"], 1u);
+
+ result = CollectSensors(impl, 2, true).Counters;
+ ASSERT_EQ(result["yt.d.sparse_counter_with_hack{}"], 1u);
+
+ result = CollectSensors(impl, 3, true).Counters;
+ ASSERT_EQ(result["yt.d.sparse_counter_with_hack{}"], 1u);
+
+ result = CollectSensors(impl, 3, true).Counters;
+ ASSERT_TRUE(result.empty());
+}
+
+TEST(TSolomonRegistry, SparseGauge)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler profiler(impl, "/d");
+
+ auto c = profiler.WithSparse().Gauge("/sparse_gauge");
+
+ auto result = CollectSensors(impl).Gauges;
+ ASSERT_TRUE(result.empty());
+
+ c.Update(1.0);
+ result = CollectSensors(impl).Gauges;
+ ASSERT_EQ(result["yt.d.sparse_gauge{}"], 1.0);
+
+ c.Update(0.0);
+ result = CollectSensors(impl).Gauges;
+ ASSERT_TRUE(result.empty());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, InvalidSensors)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto invalidTypeCounter = r.Counter("/invalid_type");
+ auto invalidTypeGauge = r.Gauge("/invalid_type");
+
+ auto invalidSettingsCounter0 = r.Counter("/invalid_settings");
+ auto invalidSettingsCounter1 = r.WithGlobal().Counter("/invalid_settings");
+
+ auto result = CollectSensors(impl);
+ ASSERT_TRUE(result.Counters.empty());
+ ASSERT_TRUE(result.Gauges.empty());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+struct TDebugProducer
+ : public ISensorProducer
+{
+ TSensorBuffer Buffer;
+
+ virtual ~TDebugProducer()
+ { }
+
+ void CollectSensors(ISensorWriter* writer) override
+ {
+ Buffer.WriteTo(writer);
+ }
+};
+
+TEST(TSolomonRegistry, GaugeProducer)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto p0 = New<TDebugProducer>();
+ r.AddProducer("/cpu", p0);
+
+ auto p1 = New<TDebugProducer>();
+ r.AddProducer("/cpu", p1);
+
+ auto result = CollectSensors(impl).Gauges;
+ ASSERT_TRUE(result.empty());
+
+ {
+ TWithTagGuard tagGuard(&p0->Buffer, "thread", "Control");
+ p0->Buffer.AddGauge("/user_time", 98);
+ p0->Buffer.AddGauge("/system_time", 15);
+ }
+
+ {
+ TWithTagGuard tagGuard(&p1->Buffer, "thread", "Profiler");
+ p1->Buffer.AddGauge("/user_time", 2);
+ p1->Buffer.AddGauge("/system_time", 25);
+ }
+
+ result = CollectSensors(impl).Gauges;
+ ASSERT_EQ(result["yt.d.cpu.user_time{thread=Control}"], 98.0);
+ ASSERT_EQ(result["yt.d.cpu.user_time{thread=Profiler}"], 2.0);
+ ASSERT_EQ(result["yt.d.cpu.user_time{}"], 100.0);
+ ASSERT_EQ(result["yt.d.cpu.system_time{thread=Control}"], 15.0);
+ ASSERT_EQ(result["yt.d.cpu.system_time{thread=Profiler}"], 25.0);
+ ASSERT_EQ(result["yt.d.cpu.system_time{}"], 40.0);
+
+ p0 = {};
+ result = CollectSensors(impl).Gauges;
+ ASSERT_EQ(result.size(), static_cast<size_t>(4));
+ ASSERT_EQ(result["yt.d.cpu.user_time{thread=Profiler}"], 2.0);
+ ASSERT_EQ(result["yt.d.cpu.user_time{}"], 2.0);
+ ASSERT_EQ(result["yt.d.cpu.system_time{thread=Profiler}"], 25.0);
+ ASSERT_EQ(result["yt.d.cpu.system_time{}"], 25.0);
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, CustomProjections)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto c0 = r.Counter("/simple_sharded");
+ c0.Increment();
+
+ auto c1 = r.Counter("/simple_sharded");
+ c1.Increment();
+
+ auto g0 = r.WithExcludedTag("node_shard", "0").Gauge("/excluded_tag");
+ g0.Update(10);
+
+ auto g1 = r.WithExcludedTag("node_shard", "1").Gauge("/excluded_tag");
+ g1.Update(20);
+
+ auto c2 = r
+ .WithRequiredTag("bundle", "sys")
+ .WithTag("table_path", "//sys/operations")
+ .Counter("/request_count");
+ c2.Increment();
+
+ auto c3 = r
+ .WithTag("medium", "ssd")
+ .WithTag("disk", "ssd0", -1)
+ .Counter("/iops");
+ c3.Increment();
+
+ auto result = CollectSensors(impl);
+ ASSERT_EQ(result.Counters["yt.d.simple_sharded{}"], 2u);
+
+ ASSERT_EQ(result.Gauges["yt.d.excluded_tag{}"], 30.0);
+ ASSERT_EQ(result.Gauges.size(), static_cast<size_t>(1));
+
+ ASSERT_EQ(result.Counters["yt.d.request_count{bundle=sys}"], 1u);
+ ASSERT_EQ(result.Counters["yt.d.request_count{bundle=sys;table_path=//sys/operations}"], 1u);
+ ASSERT_TRUE(result.Counters.find("yt.d.request_count{}") == result.Counters.end());
+ ASSERT_TRUE(result.Counters.find("yt.d.request_count{table_path=//sys/operations}") == result.Counters.end());
+
+ CollectSensors(impl, 2);
+ CollectSensors(impl, 3);
+}
+
+TEST(TSolomonRegistry, DisableProjections)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto p0 = New<TDebugProducer>();
+ r.WithProjectionsDisabled().AddProducer("/bigb", p0);
+
+ {
+ TWithTagGuard guard(&p0->Buffer, "mode", "sum");
+ p0->Buffer.AddGauge("", 10);
+ }
+
+ {
+ TWithTagGuard guard(&p0->Buffer, "mode", "percentile");
+ {
+ TWithTagGuard guard(&p0->Buffer, "p", "50");
+ p0->Buffer.AddCounter("", 20);
+ }
+ {
+ TWithTagGuard guard(&p0->Buffer, "p", "99");
+ p0->Buffer.AddCounter("", 1);
+ }
+ }
+
+ auto result = CollectSensors(impl);
+ ASSERT_EQ(1u, result.Gauges.size());
+ ASSERT_EQ(10.0, result.Gauges["yt.d.bigb{mode=sum}"]);
+
+ ASSERT_EQ(2u, result.Counters.size());
+ ASSERT_EQ(20, result.Counters["yt.d.bigb{mode=percentile;p=50}"]);
+ ASSERT_EQ(1, result.Counters["yt.d.bigb{mode=percentile;p=99}"]);
+}
+
+TEST(TSolomonRegistry, DisableRenaming)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d", "");
+
+ auto p0 = New<TDebugProducer>();
+ r.WithRenameDisabled().AddProducer("/bigb", p0);
+ p0->Buffer.AddGauge("/gauge", 10);
+ p0->Buffer.AddCounter("/counter", 5);
+
+
+ auto result = CollectSensors(impl);
+ ASSERT_EQ(1u, result.Gauges.size());
+ EXPECT_EQ(10.0, result.Gauges["/d/bigb/gauge{}"]);
+
+ ASSERT_EQ(1u, result.Counters.size());
+ EXPECT_EQ(5, result.Counters["/d/bigb/counter{}"]);
+}
+
+DECLARE_REFCOUNTED_STRUCT(TCounterProducer)
+
+struct TCounterProducer
+ : public ISensorProducer
+{
+ int i = 0;
+
+ void CollectSensors(ISensorWriter* writer) override
+ {
+ writer->AddCounter("/counter", ++i);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TCounterProducer)
+
+TEST(TSolomonRegistry, CounterProducer)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto p0 = New<TCounterProducer>();
+ r.WithProjectionsDisabled().AddProducer("", p0);
+
+ auto result = CollectSensors(impl).Counters;
+ ASSERT_EQ(1, result["yt.d.counter{}"]);
+
+ result = CollectSensors(impl).Counters;
+ ASSERT_EQ(2, result["yt.d.counter{}"]);
+
+ result = CollectSensors(impl).Counters;
+ ASSERT_EQ(3, result["yt.d.counter{}"]);
+}
+
+DECLARE_REFCOUNTED_STRUCT(TBadProducer)
+
+struct TBadProducer
+ : public ISensorProducer
+{
+ void CollectSensors(ISensorWriter*) override
+ {
+ THROW_ERROR_EXCEPTION("Unavailable");
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TBadProducer)
+
+TEST(TSolomonRegistry, Exceptions)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto producer = New<TBadProducer>();
+ r.AddProducer("/p", producer);
+ r.AddFuncCounter("/c", producer, [] () -> i64 {
+ THROW_ERROR_EXCEPTION("Unavailable");
+ });
+ r.AddFuncGauge("/g", producer, [] () -> double {
+ THROW_ERROR_EXCEPTION("Unavailable");
+ });
+
+ impl->ProcessRegistrations();
+ impl->Collect();
+}
+
+TEST(TSolomonRegistry, CounterTagsBug)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto r1 = r.WithTag("client", "1");
+
+ TTagList tags;
+ tags.emplace_back("cluster", "hahn");
+
+ auto c = r1.WithTags(TTagSet{tags}).Counter("/foo");
+ c.Increment();
+
+ impl->ProcessRegistrations();
+}
+
+TEST(TSolomonRegistry, TestRemoteTransfer)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+
+ auto remote = New<TSolomonRegistry>();
+ remote->SetWindowSize(12);
+ TProfiler r(remote, "/r");
+
+ auto c0 = r.Counter("/c");
+ c0.Increment(1);
+
+ auto d0 = r.Gauge("/d");
+ d0.Update(1.0);
+
+ auto c1 = r.TimeCounter("/t");
+ c1.Add(TDuration::Seconds(1));
+
+ auto s0 = r.Summary("/s");
+ s0.Record(1.0);
+
+ auto t0 = r.Timer("/dt");
+ t0.Record(TDuration::Seconds(1));
+
+ auto h0 = r.TimeHistogram("/h", TDuration::Zero(), TDuration::MilliSeconds(20));
+ h0.Record(TDuration::MilliSeconds(1));
+
+ remote->ProcessRegistrations();
+ remote->Collect();
+
+ auto dump = remote->DumpSensors();
+
+ TRemoteRegistry remoteRegistry(impl.Get());
+
+ impl->Collect();
+ remoteRegistry.Transfer(dump);
+
+ auto sensors = ReadSensors(impl);
+ ASSERT_EQ(1, sensors.Counters["yt.r.c{}"]);
+
+ impl->Collect();
+ remoteRegistry.Detach();
+
+ sensors = ReadSensors(impl);
+ ASSERT_TRUE(sensors.Counters.empty());
+}
+
+TEST(TSolomonRegistry, ExtensionTag)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto c0 = r.WithTag("location_type", "store")
+ .WithTag("medium", "ssd_blobs", -1)
+ .WithTag("location_id", "store0", -1)
+ .WithExtensionTag("device", "sdb", -1)
+ .WithExtensionTag("model", "M5100", -1)
+ .Counter("/bytes_read");
+ c0.Increment();
+
+ auto result = CollectSensors(impl);
+ ASSERT_EQ(result.Counters.size(), 4u);
+
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store;medium=ssd_blobs}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store;medium=ssd_blobs;location_id=store0;device=sdb;model=M5100}"));
+}
+
+TEST(TSolomonRegistry, RenameTag)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+ TProfiler r(impl, "/d");
+
+ auto tagSet = TTagSet{}
+ .WithTag({"location_type", "store"})
+ .WithTag({"medium", "ssd_blobs"}, -1)
+ .WithTag({"location_id", "store0"}, -1);
+
+ tagSet.AddExtensionTag({"device", "sdb"}, -1);
+ tagSet.AddExtensionTag({"model", "M5100"}, -1);
+
+ auto mediumTag = tagSet.AddDynamicTag(1);
+
+ auto c0 = r.WithTags(tagSet)
+ .Counter("/bytes_read");
+ c0.Increment();
+
+ auto result = CollectSensors(impl);
+ ASSERT_EQ(result.Counters.size(), 4u);
+
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store;medium=ssd_blobs}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store;medium=ssd_blobs;location_id=store0;device=sdb;model=M5100}"));
+
+ r.RenameDynamicTag(mediumTag, "medium", "default");
+ c0.Increment();
+
+ result = CollectSensors(impl);
+ ASSERT_EQ(result.Counters.size(), 4u);
+
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store;medium=default}"));
+ ASSERT_TRUE(result.Counters.contains("yt.d.bytes_read{location_type=store;location_id=store0;device=sdb;model=M5100;medium=default}"));
+}
+
+struct TBlinkingProducer
+ : ISensorProducer
+{
+ bool Report = true;
+
+ void CollectSensors(ISensorWriter* writer)
+ {
+ if (Report) {
+ writer->AddCounter("/c", 1);
+ writer->AddGauge("/g", 1);
+ }
+
+ Report = !Report;
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TBlinkingProducer)
+
+TEST(TSolomonRegistry, ProducerRemoveSupport)
+{
+ auto impl = New<TSolomonRegistry>();
+ impl->SetWindowSize(12);
+
+ TProfiler r(impl, "/d");
+
+ auto p0 = New<TBlinkingProducer>();
+ r.AddProducer("/no_remove", p0);
+
+ auto p1 = New<TBlinkingProducer>();
+ r.WithProducerRemoveSupport().AddProducer("/remove", p1);
+
+ auto result = CollectSensors(impl);
+ ASSERT_EQ(result.Counters.size(), 2u);
+ ASSERT_EQ(result.Gauges.size(), 2u);
+
+ result = CollectSensors(impl);
+ ASSERT_EQ(result.Counters.size(), 1u);
+ ASSERT_EQ(result.Gauges.size(), 1u);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/tag_ut.cpp b/yt/yt/library/profiling/unittests/tag_ut.cpp
new file mode 100644
index 0000000000..778f659f0f
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/tag_ut.cpp
@@ -0,0 +1,126 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/profiling/tag.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TTagSet, Api)
+{
+ TTagSet s1{{TTag{"foo", "bar"}}};
+ TTagSet s2{{TTag{"foo", "zog"}}};
+}
+
+TEST(TTagSet, Subsets)
+{
+ TTagIdList tags{1, 2, 3};
+
+ auto listProjections = [&] (
+ TTagIndexList parents,
+ TTagIndexList children,
+ TTagIndexList required,
+ TTagIndexList excluded,
+ TTagIndexList alternative)
+ {
+ std::vector<TTagIdList> subsets;
+ RangeSubsets(tags, parents, children, required, excluded, alternative, [&subsets] (auto list) {
+ subsets.push_back(list);
+ });
+
+ std::sort(subsets.begin(), subsets.end());
+ return subsets;
+ };
+
+ TTagIndexList noParents = {NoTagSentinel, NoTagSentinel, NoTagSentinel};
+ TTagIndexList noChildren = {NoTagSentinel, NoTagSentinel, NoTagSentinel};
+ TTagIndexList noAlternatives = {NoTagSentinel, NoTagSentinel, NoTagSentinel};
+
+ auto full = listProjections(noParents, noChildren, {}, {}, noAlternatives);
+ ASSERT_EQ(static_cast<size_t>(8), full.size());
+
+ {
+ auto actual = listProjections({NoTagSentinel, 0, 1}, noChildren, {}, {}, noAlternatives);
+ std::vector<TTagIdList> expected = {
+ { },
+ { 1 },
+ { 1, 2 },
+ { 1, 2, 3 },
+ };
+ ASSERT_EQ(expected, actual);
+ }
+
+ {
+ auto actual = listProjections({NoTagSentinel, 0, 0}, noChildren, {}, {}, noAlternatives);
+ std::vector<TTagIdList> expected = {
+ { },
+ { 1 },
+ { 1, 2 },
+ { 1, 2, 3 },
+ { 1, 3 },
+ };
+ ASSERT_EQ(expected, actual);
+ }
+
+ {
+ auto actual = listProjections(noParents, noChildren, {0}, {}, noAlternatives);
+ std::vector<TTagIdList> expected = {
+ { 1 },
+ { 1, 2 },
+ { 1, 2, 3 },
+ { 1, 3 },
+ };
+ ASSERT_EQ(expected, actual);
+ }
+
+ {
+ auto actual = listProjections(noParents, noChildren, {0, 1}, {}, noAlternatives);
+ std::vector<TTagIdList> expected = {
+ { 1, 2 },
+ { 1, 2, 3 },
+ };
+ ASSERT_EQ(expected, actual);
+ }
+
+ {
+ auto actual = listProjections(noParents, noChildren, {}, {2}, noAlternatives);
+ std::vector<TTagIdList> expected = {
+ { },
+ { 1 },
+ { 1, 2 },
+ { 2 },
+ };
+ ASSERT_EQ(expected, actual);
+ }
+
+ {
+ auto actual = listProjections(noParents, noChildren, {}, {}, {NoTagSentinel, NoTagSentinel, 1});
+ std::vector<TTagIdList> expected = {
+ { },
+ { 1 },
+ { 1, 2 },
+ { 1, 3 },
+ { 2 },
+ { 3 }
+ };
+ ASSERT_EQ(expected, actual);
+ }
+
+ {
+ auto actual = listProjections(noParents, {NoTagSentinel, 0, 0}, {}, {}, noAlternatives);
+ std::vector<TTagIdList> expected = {
+ { },
+ { 1, 2, 3 },
+ { 2 },
+ { 2, 3 },
+ { 3 }
+ };
+ ASSERT_EQ(expected, actual);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/library/profiling/unittests/ya.make b/yt/yt/library/profiling/unittests/ya.make
new file mode 100644
index 0000000000..95ae5b7b69
--- /dev/null
+++ b/yt/yt/library/profiling/unittests/ya.make
@@ -0,0 +1,35 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (OS_LINUX)
+ ALLOCATOR(TCMALLOC_256K)
+ENDIF()
+
+SRCS(
+ sensor_ut.cpp
+ name_conflicts_ut.cpp
+ profiler_ut.cpp
+ solomon_ut.cpp
+ tag_ut.cpp
+ cube_ut.cpp
+ exporter_ut.cpp
+ perf_counter_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core/test_framework
+ yt/yt/library/profiling
+ yt/yt/library/profiling/solomon
+ yt/yt/library/profiling/tcmalloc
+ yt/yt/library/profiling/resource_tracker
+ yt/yt/library/profiling/perf
+)
+
+END()
+
+RECURSE(
+ deps
+)
diff --git a/yt/yt/library/profiling/ya.make b/yt/yt/library/profiling/ya.make
new file mode 100644
index 0000000000..6e2e626564
--- /dev/null
+++ b/yt/yt/library/profiling/ya.make
@@ -0,0 +1,36 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ sensor.cpp
+ producer.cpp
+ impl.cpp
+ tag.cpp
+ testing.cpp
+ histogram_snapshot.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/assert
+ library/cpp/yt/cpu_clock
+ library/cpp/yt/small_containers
+ library/cpp/yt/string
+ library/cpp/yt/memory
+)
+
+END()
+
+RECURSE(
+ solomon
+ unittests
+ example
+ integration
+ tcmalloc
+)
+
+IF (NOT OPENSOURCE)
+ RECURSE(
+ benchmark
+ )
+ENDIF()
diff --git a/yt/yt/library/syncmap/CMakeLists.darwin-x86_64.txt b/yt/yt/library/syncmap/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..9adcb215e4
--- /dev/null
+++ b/yt/yt/library/syncmap/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,14 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-syncmap INTERFACE)
+target_link_libraries(yt-library-syncmap INTERFACE
+ contrib-libs-cxxsupp
+ yutil
+)
diff --git a/yt/yt/library/syncmap/CMakeLists.linux-aarch64.txt b/yt/yt/library/syncmap/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..c1b695ff10
--- /dev/null
+++ b/yt/yt/library/syncmap/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,15 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-syncmap INTERFACE)
+target_link_libraries(yt-library-syncmap INTERFACE
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
diff --git a/yt/yt/library/syncmap/CMakeLists.linux-x86_64.txt b/yt/yt/library/syncmap/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..c1b695ff10
--- /dev/null
+++ b/yt/yt/library/syncmap/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,15 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-syncmap INTERFACE)
+target_link_libraries(yt-library-syncmap INTERFACE
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+)
diff --git a/yt/yt/library/syncmap/CMakeLists.txt b/yt/yt/library/syncmap/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/library/syncmap/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/library/syncmap/CMakeLists.windows-x86_64.txt b/yt/yt/library/syncmap/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..9adcb215e4
--- /dev/null
+++ b/yt/yt/library/syncmap/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,14 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-syncmap INTERFACE)
+target_link_libraries(yt-library-syncmap INTERFACE
+ contrib-libs-cxxsupp
+ yutil
+)
diff --git a/yt/yt/library/syncmap/map-inl.h b/yt/yt/library/syncmap/map-inl.h
new file mode 100644
index 0000000000..9512112dd6
--- /dev/null
+++ b/yt/yt/library/syncmap/map-inl.h
@@ -0,0 +1,208 @@
+#ifndef MAP_INL_H_
+#error "Direct inclusion of this file is not allowed, include map.h"
+// For the sake of sane code completion.
+#include "map.h"
+#endif
+#undef MAP_INL_H_
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+TSyncMap<TKey, TValue, THash, TEqual, TLock>::TSyncMap()
+ : Snapshot_(new TSnapshot{})
+{ }
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+TSyncMap<TKey, TValue, THash, TEqual, TLock>::~TSyncMap()
+{
+ delete Snapshot_.load();
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+template <class TFindKey>
+TValue* TSyncMap<TKey, TValue, THash, TEqual, TLock>::Find(const TFindKey& key)
+{
+ {
+ auto snapshot = AcquireSnapshot();
+
+ if (auto it = snapshot->Map->find(key); it != snapshot->Map->end()) {
+ return &(it->second->Value);
+ }
+
+ if (!snapshot->Dirty) {
+ return nullptr;
+ }
+ }
+
+ {
+ auto guard = Guard(Lock_);
+
+ OnMiss();
+
+ auto* snapshot = Snapshot_.load();
+
+ // Do another lookup, in case dirty was promoted.
+ if (auto it = snapshot->Map->find(key); it != snapshot->Map->end()) {
+ return &(it->second->Value);
+ }
+
+ if (!snapshot->Dirty) {
+ return nullptr;
+ }
+
+ if (auto it = DirtyMap_->find(key); it != DirtyMap_->end()) {
+ return &(it->second->Value);
+ }
+
+ return nullptr;
+ }
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+template <class TCtor, class TFindKey>
+std::pair<TValue*, bool> TSyncMap<TKey, TValue, THash, TEqual, TLock>::FindOrInsert(
+ const TFindKey& key,
+ TCtor&& ctor)
+{
+ return FindOr(
+ key,
+ [] (TCtor&& ctor) {
+ return New<TEntry>(ctor());
+ },
+ std::forward<TCtor>(ctor));
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+template <class TFindKey, class... TArgs>
+std::pair<TValue*, bool> TSyncMap<TKey, TValue, THash, TEqual, TLock>::FindOrEmplace(
+ const TFindKey& key,
+ TArgs&&... args)
+{
+ return FindOr(
+ key,
+ [] (TArgs&&... args) {
+ return New<TEntry>(std::forward<TArgs>(args)...);
+ },
+ std::forward<TArgs>(args)...);
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+auto TSyncMap<TKey, TValue, THash, TEqual, TLock>::AcquireSnapshot() -> THazardPtr<TSnapshot>
+{
+ return THazardPtr<TSnapshot>::Acquire([&] {
+ return Snapshot_.load();
+ });
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+void TSyncMap<TKey, TValue, THash, TEqual, TLock>::UpdateSnapshot(TIntrusivePtr<TMap> map, bool dirty)
+{
+ if (!dirty) {
+ Misses_ = 0;
+ }
+
+ auto* newSnapshot = new TSnapshot{std::move(map), dirty};
+ auto* oldSnapshot = Snapshot_.exchange(newSnapshot);
+ RetireHazardPointer(oldSnapshot, [] (auto* ptr) {
+ delete ptr;
+ });
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+void TSyncMap<TKey, TValue, THash, TEqual, TLock>::OnMiss()
+{
+ if (!DirtyMap_) {
+ return;
+ }
+
+ Misses_++;
+ if (Misses_ < DirtyMap_->size()) {
+ return;
+ }
+
+ UpdateSnapshot(std::move(DirtyMap_), false);
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+template <class TFindKey, class TInserter, class... TArgs>
+std::pair<TValue*, bool> TSyncMap<TKey, TValue, THash, TEqual, TLock>::FindOr(
+ const TFindKey& key,
+ TInserter&& inserter,
+ TArgs&&... args)
+{
+ {
+ auto snapshot = AcquireSnapshot();
+
+ if (auto it = snapshot->Map->find(key); it != snapshot->Map->end()) {
+ return {&(it->second->Value), false};
+ }
+ }
+
+ {
+ auto guard = Guard(Lock_);
+
+ auto* snapshot = Snapshot_.load();
+ if (auto it = snapshot->Map->find(key); it != snapshot->Map->end()) {
+ OnMiss();
+ return {&(it->second->Value), false};
+ }
+
+ if (snapshot->Dirty) {
+ if (auto it = DirtyMap_->find(key); it != DirtyMap_->end()) {
+ OnMiss();
+ return {&(it->second->Value), false};
+ }
+ }
+
+ if (!snapshot->Dirty) {
+ DirtyMap_ = New<TMap>(*snapshot->Map);
+ UpdateSnapshot(snapshot->Map, true);
+ }
+
+ auto [newIt, inserted] = DirtyMap_->emplace(key, inserter(std::forward<TArgs>(args)...));
+ YT_VERIFY(inserted);
+ return {&(newIt->second->Value), true};
+ }
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+void TSyncMap<TKey, TValue, THash, TEqual, TLock>::Flush()
+{
+ {
+ auto snapshot = AcquireSnapshot();
+
+ if (!snapshot->Dirty) {
+ return;
+ }
+ }
+
+ {
+ auto guard = Guard(Lock_);
+
+ auto* snapshot = Snapshot_.load();
+
+ // Do another lookup, in case dirty was promoted.
+ if (!snapshot->Dirty) {
+ return;
+ }
+
+ UpdateSnapshot(std::move(DirtyMap_), false);
+ }
+}
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+template <class TFn>
+void TSyncMap<TKey, TValue, THash, TEqual, TLock>::IterateReadOnly(TFn&& fn)
+{
+ auto snapshot = AcquireSnapshot();
+
+ for (const auto& [key, entry] : *snapshot->Map) {
+ fn(key, entry->Value);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/library/syncmap/map.h b/yt/yt/library/syncmap/map.h
new file mode 100644
index 0000000000..7c6518769f
--- /dev/null
+++ b/yt/yt/library/syncmap/map.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <util/generic/hash.h>
+#include <util/generic/noncopyable.h>
+
+#include <yt/yt/core/misc/finally.h>
+#include <yt/yt/core/misc/hazard_ptr.h>
+#include <yt/yt/core/misc/ref_counted.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <atomic>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A thread-safe insert-only hash map that is optimized for read-mostly workloads.
+/*!
+ * When map is not modified, Find() is wait-free.
+ * After modification, next O(n) calls to Find() acquire the lock.
+ */
+template <
+ class TKey,
+ class TValue,
+ class THash = THash<TKey>,
+ class TEqual = TEqualTo<TKey>,
+ class TLock = NThreading::TSpinLock
+>
+class TSyncMap
+ : public TNonCopyable
+{
+public:
+ TSyncMap();
+
+ ~TSyncMap();
+
+ template <class TFindKey = TKey>
+ TValue* Find(const TFindKey& key);
+
+ template <class TCtor, class TFindKey = TKey>
+ std::pair<TValue*, bool> FindOrInsert(const TFindKey& key, TCtor&& ctor);
+
+ template <class TFindKey, class... TArgs>
+ std::pair<TValue*, bool> FindOrEmplace(const TFindKey& key, TArgs&&... args);
+
+ template <class TFindKey = TKey>
+ inline std::pair<TValue*, bool> FindOrDefault(const TFindKey& key);
+
+ //! Flushes dirty map. All keys inserted before this call will be moved to read-only portion of the map.
+ //! Designed to facilitate usage of IterateReadOnly.
+ void Flush();
+
+ //! IterateReadOnly iterates over read-only portion of the map.
+ template <class TFn>
+ void IterateReadOnly(TFn&& fn);
+
+private:
+ struct TEntry final
+ {
+ explicit TEntry(TValue value)
+ : Value(std::move(value))
+ { }
+
+ template <class... TArgs>
+ TEntry(TArgs&&... args)
+ : Value(std::forward<TArgs>(args)...)
+ { }
+
+ TValue Value;
+ };
+
+ struct TMap final
+ : public THashMap<TKey, TIntrusivePtr<TEntry>, THash, TEqual>
+ { };
+
+ struct TSnapshot
+ {
+ static constexpr bool EnableHazard = true;
+
+ TIntrusivePtr<TMap> Map = New<TMap>();
+ bool Dirty = false;
+ };
+
+ std::atomic<TSnapshot*> Snapshot_ = nullptr;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ TIntrusivePtr<TMap> DirtyMap_;
+
+ size_t Misses_ = 0;
+
+ void OnMiss();
+
+ THazardPtr<TSnapshot> AcquireSnapshot();
+ void UpdateSnapshot(TIntrusivePtr<TMap> map, bool dirty);
+
+ template <class TFindKey, class TInserter, class... TArgs>
+ std::pair<TValue*, bool> FindOr(const TFindKey& key, TInserter&& inserter, TArgs&&... args);
+};
+
+template <class TKey, class TValue, class THash, class TEqual, class TLock>
+template <class TFindKey>
+std::pair<TValue*, bool> TSyncMap<TKey, TValue, THash, TEqual, TLock>::FindOrDefault(const TFindKey& key)
+{
+ return FindOrEmplace(key);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define MAP_INL_H_
+#include "map-inl.h"
+#undef MAP_INL_H_
diff --git a/yt/yt/library/syncmap/unittests/map_ut.cpp b/yt/yt/library/syncmap/unittests/map_ut.cpp
new file mode 100644
index 0000000000..e316f4c8a8
--- /dev/null
+++ b/yt/yt/library/syncmap/unittests/map_ut.cpp
@@ -0,0 +1,98 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/syncmap/map.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSyncMap, SingleInsert)
+{
+ TSyncMap<int, int> map;
+
+ auto ptr = map.Find(0);
+ EXPECT_EQ(ptr, nullptr);
+
+ auto [insertedPtr, inserted] = map.FindOrInsert(0, [] { return 42; });
+ EXPECT_TRUE(inserted);
+ EXPECT_EQ(42, *insertedPtr);
+
+ for (int i = 0; i < 100; i++) {
+ auto ptr = map.Find(0);
+ EXPECT_EQ(42, *ptr);
+ }
+}
+
+TEST(TSyncMap, SingleEmplace)
+{
+ struct TTestValue
+ : public TNonCopyable
+ {
+ explicit TTestValue(int value)
+ : Value(value)
+ { }
+
+ int Value;
+ };
+ TSyncMap<int, TTestValue> map;
+
+ auto ptr = map.Find(0);
+ EXPECT_EQ(ptr, nullptr);
+
+ auto [insertedPtr, inserted] = map.FindOrEmplace(0, 42);
+ EXPECT_TRUE(inserted);
+ EXPECT_EQ(42, insertedPtr->Value);
+
+ for (int i = 0; i < 100; i++) {
+ auto ptr = map.Find(0);
+ EXPECT_EQ(42, ptr->Value);
+ }
+}
+
+TEST(TSyncMap, TestInsertLoop)
+{
+ TSyncMap<int, int> map;
+
+ for (int i = 0; i < 1000; ++i) {
+ auto [insertedPtr, inserted] = map.FindOrInsert(i, [] { return 42; });
+ EXPECT_TRUE(inserted);
+
+ for (int j = 0; j < 1000; ++j) {
+ auto ptr = map.Find(i);
+ EXPECT_TRUE(ptr);
+ EXPECT_EQ(*ptr, 42);
+ }
+ }
+}
+
+TEST(TSyncMap, TestFlush)
+{
+ TSyncMap<int, int> map;
+
+ auto [_, inserted] = map.FindOrInsert(0, [] { return 42; });
+ EXPECT_TRUE(inserted);
+
+ int elementsCount = 0;
+ for (int i = 0; i < 100; ++i) {
+ map.IterateReadOnly([&] (int, int) { ++elementsCount; });
+ EXPECT_EQ(elementsCount, 0);
+ }
+
+ for (int i = 1; i < 100; ++i) {
+ map.Flush();
+ map.IterateReadOnly(
+ [&] (int key, int value) {
+ ++elementsCount;
+ EXPECT_EQ(key, 0);
+ EXPECT_EQ(value, 42);
+ });
+ EXPECT_EQ(elementsCount, i);
+ }
+
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/library/syncmap/unittests/ya.make b/yt/yt/library/syncmap/unittests/ya.make
new file mode 100644
index 0000000000..97a41e12d5
--- /dev/null
+++ b/yt/yt/library/syncmap/unittests/ya.make
@@ -0,0 +1,14 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(map_ut.cpp)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/library/syncmap
+)
+
+END()
diff --git a/yt/yt/library/syncmap/ya.make b/yt/yt/library/syncmap/ya.make
new file mode 100644
index 0000000000..3516033aaf
--- /dev/null
+++ b/yt/yt/library/syncmap/ya.make
@@ -0,0 +1,15 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+END()
+
+RECURSE_FOR_TESTS(
+ unittests
+)
+
+IF (NOT OPENSOURCE)
+ RECURSE(
+ benchmark
+ )
+ENDIF()
diff --git a/yt/yt/library/tracing/CMakeLists.darwin-x86_64.txt b/yt/yt/library/tracing/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..54f67f1ec5
--- /dev/null
+++ b/yt/yt/library/tracing/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tracing)
+target_compile_options(yt-library-tracing PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-tracing PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-build
+ yt_proto-yt-core
+)
+target_sources(yt-library-tracing PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/tracer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/async_queue_trace.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/batch_trace.cpp
+)
diff --git a/yt/yt/library/tracing/CMakeLists.linux-aarch64.txt b/yt/yt/library/tracing/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..923b49e3e7
--- /dev/null
+++ b/yt/yt/library/tracing/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tracing)
+target_compile_options(yt-library-tracing PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-tracing PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-build
+ yt_proto-yt-core
+)
+target_sources(yt-library-tracing PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/tracer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/async_queue_trace.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/batch_trace.cpp
+)
diff --git a/yt/yt/library/tracing/CMakeLists.linux-x86_64.txt b/yt/yt/library/tracing/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..923b49e3e7
--- /dev/null
+++ b/yt/yt/library/tracing/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tracing)
+target_compile_options(yt-library-tracing PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-tracing PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-build
+ yt_proto-yt-core
+)
+target_sources(yt-library-tracing PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/tracer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/async_queue_trace.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/batch_trace.cpp
+)
diff --git a/yt/yt/library/tracing/CMakeLists.txt b/yt/yt/library/tracing/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/library/tracing/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/library/tracing/CMakeLists.windows-x86_64.txt b/yt/yt/library/tracing/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..e33cbed391
--- /dev/null
+++ b/yt/yt/library/tracing/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tracing)
+target_link_libraries(yt-library-tracing PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ yt-yt-build
+ yt_proto-yt-core
+)
+target_sources(yt-library-tracing PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/public.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/tracer.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/async_queue_trace.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tracing/batch_trace.cpp
+)
diff --git a/yt/yt/library/tracing/README.md b/yt/yt/library/tracing/README.md
new file mode 100644
index 0000000000..c8260910a1
--- /dev/null
+++ b/yt/yt/library/tracing/README.md
@@ -0,0 +1,7 @@
+# yt/yt/library/tracing
+
+## Adaptive Sampling
+
+## Coordinated Sampling
+
+
diff --git a/yt/yt/library/tracing/async_queue_trace.cpp b/yt/yt/library/tracing/async_queue_trace.cpp
new file mode 100644
index 0000000000..9af19f9188
--- /dev/null
+++ b/yt/yt/library/tracing/async_queue_trace.cpp
@@ -0,0 +1,134 @@
+#include "async_queue_trace.h"
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncQueueTrace::TAsyncQueueTrace(bool lazy)
+ : Lazy_(lazy)
+{ }
+
+void TAsyncQueueTrace::Join(i64 queueIndex, const TTraceContextPtr& context)
+{
+ if (!context->IsSampled()) {
+ return;
+ }
+
+ auto guard = Guard(Lock_);
+ YT_VERIFY(Blocked_.empty() || queueIndex > Blocked_.back().first);
+ Blocked_.emplace_back(queueIndex, context);
+
+ for (const auto& [span, startIndex] : Background_) {
+ if (queueIndex >= startIndex) {
+ context->AddAsyncChild(span->GetTraceId());
+ }
+ }
+}
+
+void TAsyncQueueTrace::Join(i64 queueIndex)
+{
+ auto* traceContext = TryGetCurrentTraceContext();
+ if (!traceContext) {
+ return;
+ }
+
+ Join(queueIndex, traceContext);
+}
+
+std::pair<TTraceContextPtr, bool> TAsyncQueueTrace::StartSpan(i64 startIndex, const TString& spanName)
+{
+ auto traceContext = TTraceContext::NewRoot(spanName);
+ traceContext->SetRecorded();
+
+ bool sampled = false;
+ if (!Lazy_) {
+ sampled = true;
+ }
+
+ auto guard = Guard(Lock_);
+
+ for (const auto& [queueIndex, client] : Blocked_) {
+ // If startIndex > queueIndex, client is not blocked by this span.
+ if (queueIndex >= startIndex && client->AddAsyncChild(traceContext->GetTraceId())) {
+ sampled = true;
+ }
+ }
+
+ if (sampled) {
+ Background_[traceContext] = startIndex;
+ traceContext->SetSampled();
+ }
+
+ return {traceContext, sampled};
+}
+
+void TAsyncQueueTrace::Commit(i64 endIndex)
+{
+ auto guard = Guard(Lock_);
+ while (!Blocked_.empty()) {
+ auto queueIndex = Blocked_.front().first;
+ if (queueIndex <= endIndex) {
+ Blocked_.pop_front();
+ } else {
+ return;
+ }
+ }
+}
+
+void TAsyncQueueTrace::FinishSpan(const TTraceContextPtr& traceContext)
+{
+ auto guard = Guard(Lock_);
+ Background_.erase(traceContext);
+}
+
+TAsyncQueueTraceGuard TAsyncQueueTrace::CreateTraceGuard(const TString& spanName, i64 startIndex, std::optional<i64> endIndex)
+{
+ auto [traceContext, sampled] = StartSpan(startIndex, spanName);
+ if (!sampled) {
+ return TAsyncQueueTraceGuard{nullptr, nullptr, {}};
+ }
+
+ return TAsyncQueueTraceGuard{this, traceContext, endIndex};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncQueueTraceGuard::TAsyncQueueTraceGuard(
+ TAsyncQueueTrace* queueTrace,
+ const TTraceContextPtr& traceContext,
+ std::optional<i64> endIndex)
+ : QueueTrace_(queueTrace)
+ , TraceContext_(traceContext)
+ , EndIndex_(endIndex)
+ , TraceContextGuard_(traceContext)
+{ }
+
+TAsyncQueueTraceGuard::TAsyncQueueTraceGuard(TAsyncQueueTraceGuard&& other)
+ : QueueTrace_(other.QueueTrace_)
+ , TraceContext_(std::move(other.TraceContext_))
+ , EndIndex_(other.EndIndex_)
+ , TraceContextGuard_(std::move(other.TraceContextGuard_))
+{
+ QueueTrace_ = nullptr;
+}
+
+TAsyncQueueTraceGuard::~TAsyncQueueTraceGuard()
+{
+ if (!QueueTrace_) {
+ return;
+ }
+
+ QueueTrace_->FinishSpan(TraceContext_);
+ if (EndIndex_) {
+ QueueTrace_->Commit(*EndIndex_);
+ }
+}
+
+void TAsyncQueueTraceGuard::OnError()
+{
+ EndIndex_ = {};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/async_queue_trace.h b/yt/yt/library/tracing/async_queue_trace.h
new file mode 100644
index 0000000000..15fa224ff3
--- /dev/null
+++ b/yt/yt/library/tracing/async_queue_trace.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <util/generic/noncopyable.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Async queue trace propagates tracing through async queue.
+//
+// We assume that multiple clients queue work, each with specific index.
+// And single background worker processes queue sequentially or with pipelining.
+// On each iteration background worker process range of items in [startIndex, endIndex].
+//
+// We assume that Join(i) is not blocked by StartSpan(j) where j > i.
+//
+// In eager mode, background worker is always traced.
+//
+// In lazy mode, background worker is traced only when there are blocked clients that request sampling.
+class TAsyncQueueTrace
+{
+public:
+ TAsyncQueueTrace(bool lazy = true);
+
+ //! Join notifies queue that context is blocked by background processing up to queueIndex.
+ //
+ // Must be called with increasing queueIndex.
+ void Join(i64 queueIndex, const TTraceContextPtr& context);
+
+ //! Same join, but with implicit trace context.
+ void Join(i64 queueIndex);
+
+ //! StartSpan creates span that traces background work in the queue.
+ std::pair<TTraceContextPtr, bool> StartSpan(i64 startIndex, const TString& spanName);
+
+ //! Notify that trace is finished.
+ void FinishSpan(const TTraceContextPtr& traceContext);
+
+ //! Notify that all future calls to StartSpan will have startIndex > endIndex.
+ void Commit(i64 endIndex);
+
+ TAsyncQueueTraceGuard CreateTraceGuard(const TString& spanName, i64 startIndex, std::optional<i64> endIndex);
+
+private:
+ const bool Lazy_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ std::deque<std::pair<i64, TTraceContextPtr>> Blocked_;
+ THashMap<TTraceContextPtr, i64> Background_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncQueueTraceGuard
+{
+public:
+ TAsyncQueueTraceGuard(TAsyncQueueTrace* queueTrace, const TTraceContextPtr& traceContext, std::optional<i64> endIndex);
+ TAsyncQueueTraceGuard(TAsyncQueueTraceGuard&& other);
+ ~TAsyncQueueTraceGuard();
+
+ // Support only move construction. Delete all other copy/move operators.
+ TAsyncQueueTraceGuard operator = (TAsyncQueueTraceGuard&& other) = delete;
+
+ void OnError();
+
+private:
+ TAsyncQueueTrace* QueueTrace_ = nullptr;
+ TTraceContextPtr TraceContext_;
+ std::optional<i64> EndIndex_;
+
+ TTraceContextGuard TraceContextGuard_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/batch_trace.cpp b/yt/yt/library/tracing/batch_trace.cpp
new file mode 100644
index 0000000000..4f81be4e12
--- /dev/null
+++ b/yt/yt/library/tracing/batch_trace.cpp
@@ -0,0 +1,44 @@
+#include "batch_trace.h"
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBatchTrace::Join()
+{
+ if (auto* context = TryGetCurrentTraceContext(); context && context->IsRecorded()) {
+ Join(MakeStrong(context));
+ }
+}
+
+void TBatchTrace::Join(const TTraceContextPtr& context)
+{
+ if (!context->IsRecorded()) {
+ return;
+ }
+
+ Clients_.push_back(context);
+}
+
+std::pair<TTraceContextPtr, bool> TBatchTrace::StartSpan(const TString& spanName)
+{
+ auto traceContext = TTraceContext::NewRoot(spanName);
+
+ bool hasBlockedClient = false;
+ for (const auto& client : Clients_) {
+ if (client->AddAsyncChild(traceContext->GetTraceId())) {
+ hasBlockedClient = true;
+ }
+ }
+ Clients_.clear();
+
+ if (hasBlockedClient) {
+ traceContext->SetSampled();
+ }
+
+ return {traceContext, hasBlockedClient};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/batch_trace.h b/yt/yt/library/tracing/batch_trace.h
new file mode 100644
index 0000000000..98d52a8685
--- /dev/null
+++ b/yt/yt/library/tracing/batch_trace.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Batch trace propagates tracing through request batching.
+/**
+ * TBatchTrace is not thread safe.
+ */
+class TBatchTrace
+{
+public:
+ void Join();
+
+ void Join(const TTraceContextPtr& context);
+
+ std::pair<TTraceContextPtr, bool> StartSpan(const TString& spanName);
+
+private:
+ std::vector<TTraceContextPtr> Clients_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/example/main.cpp b/yt/yt/library/tracing/example/main.cpp
new file mode 100644
index 0000000000..2dc5089e6f
--- /dev/null
+++ b/yt/yt/library/tracing/example/main.cpp
@@ -0,0 +1,93 @@
+#include <random>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <util/generic/yexception.h>
+
+#include <yt/yt/library/tracing/jaeger/tracer.h>
+
+using namespace NYT;
+using namespace NYT::NTracing;
+
+void SubrequestExample(std::optional<TString> endpoint)
+{
+ auto traceContext = TTraceContext::NewRoot("Example");
+ traceContext->SetSampled();
+ traceContext->AddTag("user", "prime");
+ traceContext->SetTargetEndpoint(endpoint);
+
+ traceContext->AddLogEntry(GetCpuInstant(), "Request started");
+
+ Sleep(TDuration::MilliSeconds(10));
+ auto childTraceContext = traceContext->CreateChild("Subrequest");
+ childTraceContext->AddTag("index", "0");
+
+ Sleep(TDuration::MilliSeconds(2));
+ childTraceContext->Finish();
+
+ Sleep(TDuration::MilliSeconds(2));
+ traceContext->AddLogEntry(GetCpuInstant(), "Request finished");
+ traceContext->Finish();
+
+ Cout << ToString(traceContext->GetTraceId()) << Endl;
+}
+
+void DelayedSamplingExample(std::optional<TString> endpoint)
+{
+ auto traceContext = TTraceContext::NewRoot("Job");
+ traceContext->SetRecorded();
+ traceContext->SetTargetEndpoint(endpoint);
+
+ auto fastRequestContext = traceContext->CreateChild("FastRequest");
+ fastRequestContext->Finish();
+
+ auto startContext = traceContext->CreateChild("Start");
+ startContext->Finish();
+
+ auto slowRequestContext = startContext->CreateChild("SlowRequest");
+
+ traceContext->SetSampled();
+ YT_VERIFY(slowRequestContext->IsSampled());
+
+ slowRequestContext->Finish();
+ traceContext->Finish();
+}
+
+int main(int argc, char* argv[])
+{
+ try {
+ if (argc < 2) {
+ throw yexception() << "usage: " << argv[0] << " COLLECTOR_ENDPOINTS";
+ }
+
+ auto config = New<NTracing::TJaegerTracerConfig>();
+ config->CollectorChannelConfig = New<NRpc::NGrpc::TChannelConfig>();
+ config->CollectorChannelConfig->Address = argv[1];
+
+ config->FlushPeriod = TDuration::MilliSeconds(100);
+
+ config->ServiceName = "example";
+ config->ProcessTags["host"] = "prime-dev.qyp.yandex-team.ru";
+
+ auto jaeger = New<NTracing::TJaegerTracer>(config);
+ SetGlobalTracer(jaeger);
+
+ for (int i = 1; i < argc; ++i) {
+ std::optional<TString> endpoint;
+ if (i != 1) {
+ endpoint = argv[i];
+ }
+
+ SubrequestExample(endpoint);
+
+ DelayedSamplingExample(endpoint);
+ }
+
+ jaeger->WaitFlush().Get();
+ } catch (const std::exception& ex) {
+ Cerr << ex.what() << Endl;
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/yt/yt/library/tracing/example/ya.make b/yt/yt/library/tracing/example/ya.make
new file mode 100644
index 0000000000..beeb55fe0c
--- /dev/null
+++ b/yt/yt/library/tracing/example/ya.make
@@ -0,0 +1,11 @@
+PROGRAM(tracing-example)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(main.cpp)
+
+PEERDIR(
+ yt/yt/library/tracing/jaeger
+)
+
+END()
diff --git a/yt/yt/library/tracing/jaeger/model.proto b/yt/yt/library/tracing/jaeger/model.proto
new file mode 100644
index 0000000000..251a78b178
--- /dev/null
+++ b/yt/yt/library/tracing/jaeger/model.proto
@@ -0,0 +1,79 @@
+syntax="proto3";
+
+package NYT.NTracing.NProto;
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+
+enum ValueType {
+ STRING = 0;
+ BOOL = 1;
+ INT64 = 2;
+ FLOAT64 = 3;
+ BINARY = 4;
+};
+
+message KeyValue {
+ string key = 1;
+ ValueType v_type = 2;
+ string v_str = 3;
+ bool v_bool = 4;
+ int64 v_int64 = 5;
+ double v_float64 = 6;
+ bytes v_binary = 7;
+}
+
+message Log {
+ google.protobuf.Timestamp timestamp = 1;
+ repeated KeyValue fields = 2;
+}
+
+enum SpanRefType {
+ CHILD_OF = 0;
+ FOLLOWS_FROM = 1;
+};
+
+message SpanRef {
+ bytes trace_id = 1;
+ bytes span_id = 2;
+ SpanRefType ref_type = 3;
+}
+
+message Process {
+ string service_name = 1;
+ repeated KeyValue tags = 2;
+}
+
+message Span {
+ bytes trace_id = 1;
+ bytes span_id = 2;
+ string operation_name = 3;
+ repeated SpanRef references = 4;
+ uint32 flags = 5;
+ google.protobuf.Timestamp start_time = 6;
+ google.protobuf.Duration duration = 7;
+ repeated KeyValue tags = 8;
+ repeated Log logs = 9;
+ Process process = 10;
+ string process_id = 11;
+ repeated string warnings = 12;
+}
+
+message Batch {
+ repeated Span spans = 1;
+ Process process = 2;
+}
+
+message DependencyLink {
+ string parent = 1;
+ string child = 2;
+ uint64 call_count = 3;
+ string source = 4;
+}
+
+message TReqPostSpans {
+ bytes batch = 1;
+}
+
+message TRspPostSpans {
+}
diff --git a/yt/yt/library/tracing/jaeger/public.h b/yt/yt/library/tracing/jaeger/public.h
new file mode 100644
index 0000000000..1d9506cacf
--- /dev/null
+++ b/yt/yt/library/tracing/jaeger/public.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TSampler)
+DECLARE_REFCOUNTED_CLASS(TSamplerConfig)
+
+DECLARE_REFCOUNTED_CLASS(TJaegerTracerDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TJaegerTracerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/jaeger/sampler.cpp b/yt/yt/library/tracing/jaeger/sampler.cpp
new file mode 100644
index 0000000000..62f0cb9d50
--- /dev/null
+++ b/yt/yt/library/tracing/jaeger/sampler.cpp
@@ -0,0 +1,125 @@
+#include "sampler.h"
+
+namespace NYT::NTracing {
+
+using namespace NConcurrency;
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const NProfiling::TProfiler Profiler{"/jaeger"};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSamplerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("global_sample_rate", &TThis::GlobalSampleRate)
+ .Default(0.0);
+ registrar.Parameter("user_sample_rate", &TThis::UserSampleRate)
+ .Default();
+ registrar.Parameter("user_endpoints", &TThis::UserEndpoint)
+ .Default();
+ registrar.Parameter("clear_sampled_flag", &TThis::ClearSampledFlag)
+ .Default();
+
+ registrar.Parameter("min_per_user_samples", &TThis::MinPerUserSamples)
+ .Default(0);
+ registrar.Parameter("min_per_user_samples_period", &TThis::MinPerUserSamplesPeriod)
+ .Default(TDuration::Minutes(1));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TSampler::TUserState::TrySampleByMinCount(ui64 minCount, TCpuDuration period)
+{
+ if (minCount == 0) {
+ return false;
+ }
+
+ auto lastReset = LastReset.load();
+ auto now = GetCpuInstant();
+ if (now - lastReset > period) {
+ if (LastReset.compare_exchange_strong(lastReset, now)) {
+ Sampled.store(0);
+ }
+ }
+
+ return Sampled.fetch_add(1) < minCount;
+}
+
+TSampler::TSampler()
+ : Config_(New<TSamplerConfig>())
+ , TracesSampled_(Profiler.WithHot().Counter("/traces_sampled"))
+{ }
+
+TSampler::TSampler(const TSamplerConfigPtr& config)
+ : Config_(config)
+{ }
+
+void TSampler::SampleTraceContext(const TString& user, const TTraceContextPtr& traceContext)
+{
+ auto config = Config_.Acquire();
+
+ auto [userState, inserted] = Users_.FindOrInsert(user, [&user] {
+ auto state = New<TUserState>();
+
+ auto profiler = Profiler.WithSparse().WithHot().WithTag("user", user);
+ state->TracesSampledByUser = profiler.WithSparse().Counter("/traces_sampled_by_user");
+ state->TracesSampledByProbability = profiler.WithSparse().Counter("/traces_sampled_by_probability");
+
+ return state;
+ });
+
+ std::optional<TString> endpoint;
+ auto itEndpoint = config->UserEndpoint.find(user);
+ if (itEndpoint != config->UserEndpoint.end()) {
+ traceContext->SetTargetEndpoint(itEndpoint->second);
+ }
+
+ if (traceContext->IsSampled()) {
+ userState->Get()->TracesSampledByUser.Increment();
+
+ if (config->ClearSampledFlag.find(user) != config->ClearSampledFlag.end()) {
+ traceContext->SetSampled(false);
+ } else {
+ TracesSampled_.Increment();
+ return;
+ }
+ }
+
+ if (config->GlobalSampleRate != 0.0) {
+ auto p = RandomNumber<double>();
+ if (p < config->GlobalSampleRate) {
+ userState->Get()->TracesSampledByProbability.Increment();
+ TracesSampled_.Increment();
+ traceContext->SetSampled(true);
+ return;
+ }
+ }
+
+ auto it = config->UserSampleRate.find(user);
+ if (it != config->UserSampleRate.end()) {
+ auto p = RandomNumber<double>();
+ if (p < it->second) {
+ userState->Get()->TracesSampledByProbability.Increment();
+ TracesSampled_.Increment();
+ traceContext->SetSampled(true);
+ return;
+ }
+ }
+
+ if (userState->Get()->TrySampleByMinCount(config->MinPerUserSamples, DurationToCpuDuration(config->MinPerUserSamplesPeriod))) {
+ TracesSampled_.Increment();
+ traceContext->SetSampled(true);
+ return;
+ }
+}
+
+void TSampler::UpdateConfig(const TSamplerConfigPtr& config)
+{
+ Config_.Store(config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/jaeger/sampler.h b/yt/yt/library/tracing/jaeger/sampler.h
new file mode 100644
index 0000000000..57119f0441
--- /dev/null
+++ b/yt/yt/library/tracing/jaeger/sampler.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <yt/yt/library/syncmap/map.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSamplerConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ //! Request is sampled with probability P.
+ double GlobalSampleRate;
+
+ //! Additionally, request is sampled with probability P(user).
+ THashMap<TString, double> UserSampleRate;
+
+ //! Spans are sent to specified endpoint.
+ THashMap<TString, TString> UserEndpoint;
+
+ //! Additionally, sample first N requests for each user in the window.
+ ui64 MinPerUserSamples;
+ TDuration MinPerUserSamplesPeriod;
+
+ //! Clear sampled from from incoming user request.
+ THashMap<TString, bool> ClearSampledFlag;
+
+ REGISTER_YSON_STRUCT(TSamplerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSamplerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSampler
+ : public TRefCounted
+{
+public:
+ TSampler();
+ explicit TSampler(const TSamplerConfigPtr& config);
+
+ void SampleTraceContext(const TString& user, const TTraceContextPtr& traceContext);
+
+ void UpdateConfig(const TSamplerConfigPtr& config);
+
+private:
+ TAtomicIntrusivePtr<TSamplerConfig> Config_;
+
+ struct TUserState final
+ {
+ std::atomic<ui64> Sampled = {0};
+ std::atomic<NProfiling::TCpuInstant> LastReset = {0};
+
+ bool TrySampleByMinCount(ui64 minCount, NProfiling::TCpuDuration period);
+
+ NProfiling::TCounter TracesSampledByUser;
+ NProfiling::TCounter TracesSampledByProbability;
+ };
+
+ NConcurrency::TSyncMap<TString, TIntrusivePtr<TUserState>> Users_;
+ NProfiling::TCounter TracesSampled_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSampler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/jaeger/tracer.cpp b/yt/yt/library/tracing/jaeger/tracer.cpp
new file mode 100644
index 0000000000..54a00e5005
--- /dev/null
+++ b/yt/yt/library/tracing/jaeger/tracer.cpp
@@ -0,0 +1,616 @@
+#include "tracer.h"
+
+#include <yt/yt/library/tracing/jaeger/model.pb.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <yt/yt/core/rpc/grpc/channel.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/serialize.h>
+#include <yt/yt/core/utilex/random.h>
+
+#include <util/string/cast.h>
+#include <util/string/reverse.h>
+
+#include <util/system/getpid.h>
+#include <util/system/env.h>
+#include <util/system/byteorder.h>
+
+namespace NYT::NTracing {
+
+using namespace NRpc;
+using namespace NConcurrency;
+using namespace NProfiling;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const NLogging::TLogger Logger{"Jaeger"};
+static const NProfiling::TProfiler Profiler{"/tracing"};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TJaegerTracerDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("collector_channel_config", &TThis::CollectorChannelConfig)
+ .Optional();
+ registrar.Parameter("max_request_size", &TThis::MaxRequestSize)
+ .Default();
+ registrar.Parameter("max_memory", &TThis::MaxMemory)
+ .Default();
+ registrar.Parameter("subsampling_rate", &TThis::SubsamplingRate)
+ .Default();
+ registrar.Parameter("flush_period", &TThis::FlushPeriod)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TJaegerTracerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("collector_channel_config", &TThis::CollectorChannelConfig)
+ .Optional();
+
+ // 10K nodes x 128 KB / 15s == 85mb/s
+ registrar.Parameter("flush_period", &TThis::FlushPeriod)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("stop_timeout", &TThis::StopTimeout)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("rpc_timeout", &TThis::RpcTimeout)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("queue_stall_timeout", &TThis::QueueStallTimeout)
+ .Default(TDuration::Minutes(15));
+ registrar.Parameter("max_request_size", &TThis::MaxRequestSize)
+ .Default(128_KB)
+ .LessThanOrEqual(4_MB);
+ registrar.Parameter("max_batch_size", &TThis::MaxBatchSize)
+ .Default(128);
+ registrar.Parameter("max_memory", &TThis::MaxMemory)
+ .Default(1_GB);
+ registrar.Parameter("subsampling_rate", &TThis::SubsamplingRate)
+ .Default();
+ registrar.Parameter("reconnect_period", &TThis::ReconnectPeriod)
+ .Default(TDuration::Minutes(15));
+ registrar.Parameter("endpoint_channel_timeout", &TThis::EndpointChannelTimeout)
+ .Default(TDuration::Hours(2));
+
+ registrar.Parameter("service_name", &TThis::ServiceName)
+ .Default();
+ registrar.Parameter("process_tags", &TThis::ProcessTags)
+ .Default();
+ registrar.Parameter("enable_pid_tag", &TThis::EnablePidTag)
+ .Default(false);
+}
+
+TJaegerTracerConfigPtr TJaegerTracerConfig::ApplyDynamic(const TJaegerTracerDynamicConfigPtr& dynamicConfig) const
+{
+ auto config = New<TJaegerTracerConfig>();
+ config->CollectorChannelConfig = CollectorChannelConfig;
+ if (dynamicConfig->CollectorChannelConfig) {
+ config->CollectorChannelConfig = dynamicConfig->CollectorChannelConfig;
+ }
+
+ config->FlushPeriod = dynamicConfig->FlushPeriod.value_or(FlushPeriod);
+ config->QueueStallTimeout = QueueStallTimeout;
+ config->MaxRequestSize = dynamicConfig->MaxRequestSize.value_or(MaxRequestSize);
+ config->MaxBatchSize = MaxBatchSize;
+ config->MaxMemory = dynamicConfig->MaxMemory.value_or(MaxMemory);
+ config->SubsamplingRate = SubsamplingRate;
+ if (dynamicConfig->SubsamplingRate) {
+ config->SubsamplingRate = dynamicConfig->SubsamplingRate;
+ }
+
+ config->ServiceName = ServiceName;
+ config->ProcessTags = ProcessTags;
+ config->EnablePidTag = EnablePidTag;
+
+ config->Postprocess();
+ return config;
+}
+
+bool TJaegerTracerConfig::IsEnabled() const
+{
+ return ServiceName && CollectorChannelConfig;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJaegerCollectorProxy
+ : public TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TJaegerCollectorProxy, jaeger.api_v2.CollectorService);
+
+ DEFINE_RPC_PROXY_METHOD(NProto, PostSpans);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+void ToProtoGuid(TString* proto, const TGuid& guid)
+{
+ *proto = TString{reinterpret_cast<const char*>(&guid.Parts32[0]), 16};
+ ReverseInPlace(*proto);
+}
+
+void ToProtoUInt64(TString* proto, i64 i)
+{
+ i = SwapBytes64(i);
+ *proto = TString{reinterpret_cast<char*>(&i), 8};
+}
+
+void ToProto(NProto::Span* proto, const TTraceContextPtr& traceContext)
+{
+ ToProtoGuid(proto->mutable_trace_id(), traceContext->GetTraceId());
+ ToProtoUInt64(proto->mutable_span_id(), traceContext->GetSpanId());
+
+ proto->set_operation_name(traceContext->GetSpanName());
+
+ proto->mutable_start_time()->set_seconds(traceContext->GetStartTime().Seconds());
+ proto->mutable_start_time()->set_nanos(traceContext->GetStartTime().NanoSecondsOfSecond());
+
+ proto->mutable_duration()->set_seconds(traceContext->GetDuration().Seconds());
+ proto->mutable_duration()->set_nanos(traceContext->GetDuration().NanoSecondsOfSecond());
+
+ for (const auto& [name, value] : traceContext->GetTags()) {
+ auto* protoTag = proto->add_tags();
+
+ protoTag->set_key(name);
+ protoTag->set_v_str(value);
+ }
+
+ for (const auto& logEntry : traceContext->GetLogEntries()) {
+ auto* log = proto->add_logs();
+
+ auto at = CpuInstantToInstant(logEntry.At);
+ log->mutable_timestamp()->set_seconds(at.Seconds());
+ log->mutable_timestamp()->set_nanos(at.NanoSecondsOfSecond());
+
+ auto* message = log->add_fields();
+
+ message->set_key("message");
+ message->set_v_str(logEntry.Message);
+ }
+
+ int i = 0;
+ for (const auto& traceId : traceContext->GetAsyncChildren()) {
+ auto* tag = proto->add_tags();
+
+ tag->set_key(Format("yt.async_trace_id.%d", i++));
+ tag->set_v_str(ToString(traceId));
+ }
+
+ if (auto parentSpanId = traceContext->GetParentSpanId(); parentSpanId != InvalidSpanId) {
+ auto* ref = proto->add_references();
+
+ ToProtoGuid(ref->mutable_trace_id(), traceContext->GetTraceId());
+ ToProtoUInt64(ref->mutable_span_id(), parentSpanId);
+ ref->set_ref_type(NProto::CHILD_OF);
+ }
+}
+
+template<typename TK, typename TV>
+std::vector<TK> ExtractKeys(THashMap<TK, TV> const& inputMap) {
+ std::vector<TK> retval;
+ for (auto const& element : inputMap) {
+ retval.push_back(element.first);
+ }
+ return retval;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBatchInfo::TBatchInfo()
+{ }
+
+TBatchInfo::TBatchInfo(const TString& endpoint)
+ : TracesDequeued_(Profiler.WithTag("endpoint", endpoint).Counter("/traces_dequeued"))
+ , TracesDropped_(Profiler.WithTag("endpoint", endpoint).Counter("/traces_dropped"))
+ , MemoryUsage_(Profiler.WithTag("endpoint", endpoint).Gauge("/memory_usage"))
+ , TraceQueueSize_(Profiler.WithTag("endpoint", endpoint).Gauge("/queue_size"))
+{ }
+
+void TBatchInfo::PopFront()
+{
+ QueueMemory_ -= BatchQueue_[0].second.Size();
+ QueueSize_ -= BatchQueue_[0].first;
+
+ BatchQueue_.pop_front();
+
+ MemoryUsage_.Update(QueueMemory_);
+ TraceQueueSize_.Update(QueueSize_);
+}
+
+void TBatchInfo::EmplaceBack(int size, NYT::TSharedRef&& value)
+{
+ QueueMemory_ += value.size();
+ QueueSize_ += size;
+
+ BatchQueue_.emplace_back(size, value);
+
+ MemoryUsage_.Update(QueueMemory_);
+ TraceQueueSize_.Update(QueueSize_);
+ TracesDequeued_.Increment(size);
+}
+
+std::pair<i64, i64> TBatchInfo::DropQueue(int batchCount)
+{
+ auto oldMemory = QueueMemory_;
+ auto oldSize = QueueSize_;
+ for (; batchCount > 0; batchCount--) {
+ PopFront();
+ }
+
+ return {QueueMemory_ - oldMemory, QueueSize_ - oldSize};
+}
+
+void TBatchInfo::IncrementTracesDropped(i64 delta)
+{
+ TracesDropped_.Increment(delta);
+}
+
+std::tuple<std::vector<TSharedRef>, int, int> TBatchInfo::PeekQueue(const TJaegerTracerConfigPtr& config, std::optional<TSharedRef> processInfo)
+{
+ std::vector<TSharedRef> batches;
+ if (processInfo) {
+ batches.push_back(processInfo.value());
+ }
+
+ i64 memorySize = 0;
+ int spanCount = 0;
+ int batchCount = 0;
+
+ for (; batchCount < std::ssize(BatchQueue_); batchCount++) {
+ if (config && memorySize > config->MaxRequestSize) {
+ break;
+ }
+
+ memorySize += BatchQueue_[batchCount].second.Size();
+ spanCount += BatchQueue_[batchCount].first;
+ batches.push_back(BatchQueue_[batchCount].second);
+ }
+
+ return std::make_tuple(batches, batchCount, spanCount);
+}
+
+TJaegerChannelManager::TJaegerChannelManager()
+ : Channel_()
+ , ReopenTime_(TInstant::Now())
+ , RpcTimeout_()
+{ }
+
+TJaegerChannelManager::TJaegerChannelManager(const TIntrusivePtr<TJaegerTracerConfig>& config, const TString& endpoint)
+ : Endpoint_(endpoint)
+ , ReopenTime_(TInstant::Now() + config->ReconnectPeriod + RandomDuration(config->ReconnectPeriod))
+ , RpcTimeout_(config->RpcTimeout)
+ , PushedBytes_(Profiler.WithTag("endpoint", endpoint).Counter("/pushed_bytes"))
+ , PushErrors_(Profiler.WithTag("endpoint", endpoint).Counter("/push_errors"))
+ , PayloadSize_(Profiler.WithTag("endpoint", endpoint).Summary("/payload_size"))
+ , PushDuration_(Profiler.WithTag("endpoint", endpoint).Timer("/push_duration"))
+{
+ auto channelEndpointConfig = CloneYsonStruct(config->CollectorChannelConfig);
+ channelEndpointConfig->Address = endpoint;
+
+ Channel_ = NGrpc::CreateGrpcChannel(channelEndpointConfig);
+}
+
+bool TJaegerChannelManager::Push(const std::vector<TSharedRef>& batches, int spanCount)
+{
+ try {
+ TJaegerCollectorProxy proxy(Channel_);
+ proxy.SetDefaultTimeout(RpcTimeout_);
+
+ auto req = proxy.PostSpans();
+ req->SetEnableLegacyRpcCodecs(false);
+ req->set_batch(MergeRefsToString(batches));
+
+ YT_LOG_DEBUG("Sending spans (SpanCount: %v, PayloadSize: %v, Endpoint: %v)",
+ spanCount,
+ req->batch().size(),
+ Endpoint_);
+
+ TEventTimerGuard timerGuard(PushDuration_);
+ WaitFor(req->Invoke())
+ .ThrowOnError();
+
+ PushedBytes_.Increment(req->batch().size());
+ PayloadSize_.Record(req->batch().size());
+ } catch (const std::exception& ex) {
+ PushErrors_.Increment();
+ YT_LOG_ERROR(ex, "Failed to send spans (Endpoint: %v)", Endpoint_);
+ return false;
+ }
+
+ return true;
+}
+
+bool TJaegerChannelManager::NeedsReopen(TInstant currentTime)
+{
+ if (Channel_ && currentTime >= ReopenTime_) {
+ Channel_.Reset();
+ }
+
+ return !Channel_;
+}
+
+void TJaegerChannelManager::ForceReset(TInstant currentTime)
+{
+ currentTime = currentTime - TDuration::Minutes(1);
+}
+
+TInstant TJaegerChannelManager::GetReopenTime()
+{
+ return ReopenTime_;
+}
+
+TJaegerTracer::TJaegerTracer(
+ const TJaegerTracerConfigPtr& config)
+ : ActionQueue_(New<TActionQueue>("Jaeger"))
+ , FlushExecutor_(New<TPeriodicExecutor>(
+ ActionQueue_->GetInvoker(),
+ BIND(&TJaegerTracer::Flush, MakeStrong(this)),
+ config->FlushPeriod))
+ , Config_(config)
+{
+ Profiler.AddFuncGauge("/enabled", MakeStrong(this), [this] {
+ return Config_.Acquire()->IsEnabled();
+ });
+
+ FlushExecutor_->Start();
+}
+
+TFuture<void> TJaegerTracer::WaitFlush()
+{
+ return QueueEmptyPromise_.Load().ToFuture();
+}
+
+void TJaegerTracer::NotifyEmptyQueue()
+{
+ if (!TraceQueue_.IsEmpty() || TotalSize_ != 0) {
+ return;
+ }
+
+ QueueEmptyPromise_.Exchange(NewPromise<void>()).Set();
+}
+
+void TJaegerTracer::Stop()
+{
+ YT_LOG_INFO("Stopping tracer");
+
+ auto flushFuture = WaitFlush();
+ FlushExecutor_->ScheduleOutOfBand();
+
+ auto config = Config_.Acquire();
+ Y_UNUSED(WaitFor(flushFuture.WithTimeout(config->StopTimeout)));
+
+ YT_UNUSED_FUTURE(FlushExecutor_->Stop());
+ ActionQueue_->Shutdown();
+
+ YT_LOG_INFO("Tracer stopped");
+}
+
+void TJaegerTracer::Configure(const TJaegerTracerConfigPtr& config)
+{
+ Config_.Store(config);
+ FlushExecutor_->SetPeriod(config->FlushPeriod);
+}
+
+void TJaegerTracer::Enqueue(TTraceContextPtr trace)
+{
+ TraceQueue_.Enqueue(std::move(trace));
+}
+
+void TJaegerTracer::DequeueAll(const TJaegerTracerConfigPtr& config)
+{
+ auto traces = TraceQueue_.DequeueAll();
+ if (traces.empty() || !OpenChannelConfig_) {
+ return;
+ }
+
+ THashMap<TString, NProto::Batch> batches;
+ auto flushBatch = [&](TString endpoint) {
+ auto itBatch = batches.find(endpoint);
+ if (itBatch == batches.end()) {
+ return;
+ }
+ auto& batch = itBatch->second;
+ if (batch.spans_size() == 0) {
+ return;
+ }
+
+ auto itInfo = BatchInfo_.find(endpoint);
+ if (itInfo == BatchInfo_.end()) {
+ itInfo = BatchInfo_.insert({endpoint, TBatchInfo(endpoint)}).first;
+ }
+ auto& currentInfo = itInfo->second;
+
+ if (TotalMemory_ > config->MaxMemory) {
+ currentInfo.IncrementTracesDropped(batch.spans_size());
+ batch.Clear();
+ return;
+ }
+
+ auto tmpProto = SerializeProtoToRef(batch);
+
+ auto sizeDelta = batch.spans_size();
+ TotalMemory_ += tmpProto.size();
+
+ currentInfo.EmplaceBack(sizeDelta, std::move(tmpProto));
+
+ TotalSize_ += sizeDelta;
+
+ batch.Clear();
+ };
+
+ for (const auto& trace : traces) {
+ if (config->SubsamplingRate) {
+ auto traceHash = THash<TGuid>()(trace->GetTraceId());
+
+ if (((traceHash % 128) / 128.) > *config->SubsamplingRate) {
+ continue;
+ }
+ }
+
+ auto targetEndpoint = trace->GetTargetEndpoint();
+ auto endpoint = targetEndpoint.value_or(OpenChannelConfig_->Address);
+
+ ToProto(batches[endpoint].add_spans(), trace);
+
+ if (batches[endpoint].spans_size() > config->MaxBatchSize) {
+ flushBatch(endpoint);
+ }
+ }
+
+ auto keys = ExtractKeys(batches);
+ for (const auto& endpoint : keys) {
+ flushBatch(endpoint);
+ }
+}
+
+std::tuple<std::vector<TSharedRef>, int, int> TJaegerTracer::PeekQueue(const TJaegerTracerConfigPtr& config, const TString& endpoint)
+{
+ auto it = BatchInfo_.find(endpoint);
+ if (it == BatchInfo_.end()) {
+ return {{}, 0, 0};
+ }
+
+ std::optional<TSharedRef> processInfo;
+ if (config && config->IsEnabled()) {
+ processInfo = GetProcessInfo(config);
+ }
+
+ return it->second.PeekQueue(config, processInfo);
+}
+
+void TJaegerTracer::DropQueue(int batchCount, const TString& endpoint)
+{
+ auto it = BatchInfo_.find(endpoint);
+ if (it == BatchInfo_.end()) {
+ return;
+ }
+
+ auto& batchInfo = it->second;
+
+ auto [memoryDelta, sizeDelta] = batchInfo.DropQueue(batchCount);
+
+ TotalMemory_ += memoryDelta;
+ TotalSize_ += sizeDelta;
+}
+
+void TJaegerTracer::DropFullQueue()
+{
+ auto keys = ExtractKeys(BatchInfo_);
+ for (const auto& endpoint : keys) {
+ while (true) {
+ auto [batches, batchCount, spanCount] = PeekQueue(nullptr, endpoint);
+ BatchInfo_[endpoint].IncrementTracesDropped(spanCount);
+ DropQueue(batchCount, endpoint);
+
+ if (batchCount == 0) {
+ break;
+ }
+ }
+ }
+}
+
+void TJaegerTracer::Flush()
+{
+ YT_LOG_DEBUG("Started span flush");
+
+ auto config = Config_.Acquire();
+
+ DequeueAll(config);
+
+ if (TInstant::Now() - LastSuccessfullFlushTime_ > config->QueueStallTimeout) {
+ DropFullQueue();
+ }
+
+ if (!config->IsEnabled()) {
+ YT_LOG_DEBUG("Tracer is disabled");
+ DropFullQueue();
+ NotifyEmptyQueue();
+ return;
+ }
+
+ auto flushStartTime = TInstant::Now();
+
+ if (OpenChannelConfig_ != config->CollectorChannelConfig) {
+ OpenChannelConfig_ = config->CollectorChannelConfig;
+ for (auto& [endpoint, channel] : CollectorChannels_) {
+ CollectorChannels_[endpoint].ForceReset(flushStartTime);
+ }
+ }
+
+ std::stack<TString> toRemove;
+ auto keys = ExtractKeys(BatchInfo_);
+ for (const auto& endpoint : keys) {
+ auto [batches, batchCount, spanCount] = PeekQueue(config, endpoint);
+ if (batchCount <= 0) {
+ if (!CollectorChannels_.contains(endpoint) || flushStartTime > CollectorChannels_[endpoint].GetReopenTime() + config->EndpointChannelTimeout) {
+ toRemove.push(endpoint);
+ }
+ YT_LOG_DEBUG("Span queue is empty (Endpoint: %v)", endpoint);
+ continue;
+ }
+
+ auto it = CollectorChannels_.find(endpoint);
+ if (it == CollectorChannels_.end()) {
+ it = CollectorChannels_.insert({endpoint, TJaegerChannelManager(config, endpoint)}).first;
+ }
+
+ auto& channel = it->second;
+
+ if (channel.NeedsReopen(flushStartTime)) {
+ channel = TJaegerChannelManager(config, endpoint);
+ }
+
+ if (channel.Push(batches, spanCount)) {
+ DropQueue(batchCount, endpoint);
+ YT_LOG_DEBUG("Spans sent (Endpoint: %v)", endpoint);
+ LastSuccessfullFlushTime_ = flushStartTime;
+ }
+ }
+
+ while (!toRemove.empty()) {
+ auto endpoint = toRemove.top();
+ toRemove.pop();
+
+ CollectorChannels_.erase(endpoint);
+ BatchInfo_.erase(endpoint);
+ }
+
+ NotifyEmptyQueue();
+}
+
+TSharedRef TJaegerTracer::GetProcessInfo(const TJaegerTracerConfigPtr& config)
+{
+ NProto::Batch batch;
+ auto* process = batch.mutable_process();
+ process->set_service_name(*config->ServiceName);
+
+ for (const auto& [key, value] : config->ProcessTags) {
+ auto* tag = process->add_tags();
+ tag->set_key(key);
+ tag->set_v_str(value);
+ }
+
+ if (config->EnablePidTag) {
+ auto* tag = process->add_tags();
+ tag->set_key("pid");
+ tag->set_v_str(::ToString(GetPID()));
+ }
+
+ return SerializeProtoToRef(batch);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/jaeger/tracer.h b/yt/yt/library/tracing/jaeger/tracer.h
new file mode 100644
index 0000000000..adf32c11d2
--- /dev/null
+++ b/yt/yt/library/tracing/jaeger/tracer.h
@@ -0,0 +1,193 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/library/tracing/tracer.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <yt/yt/core/misc/mpsc_stack.h>
+#include <yt/yt/core/misc/atomic_object.h>
+
+#include <yt/yt/core/rpc/grpc/config.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJaegerTracerDynamicConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NRpc::NGrpc::TChannelConfigPtr CollectorChannelConfig;
+
+ std::optional<i64> MaxRequestSize;
+
+ std::optional<i64> MaxMemory;
+
+ std::optional<double> SubsamplingRate;
+
+ std::optional<TDuration> FlushPeriod;
+
+ REGISTER_YSON_STRUCT(TJaegerTracerDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TJaegerTracerDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJaegerTracerConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NRpc::NGrpc::TChannelConfigPtr CollectorChannelConfig;
+
+ TDuration FlushPeriod;
+
+ TDuration StopTimeout;
+
+ TDuration RpcTimeout;
+
+ TDuration EndpointChannelTimeout;
+
+ TDuration QueueStallTimeout;
+
+ TDuration ReconnectPeriod;
+
+ i64 MaxRequestSize;
+
+ i64 MaxBatchSize;
+
+ i64 MaxMemory;
+
+ std::optional<double> SubsamplingRate;
+
+ // ServiceName is required by jaeger. When ServiceName is missing, tracer is disabled.
+ std::optional<TString> ServiceName;
+
+ THashMap<TString, TString> ProcessTags;
+
+ bool EnablePidTag;
+
+ TJaegerTracerConfigPtr ApplyDynamic(const TJaegerTracerDynamicConfigPtr& dynamicConfig) const;
+
+ bool IsEnabled() const;
+
+ REGISTER_YSON_STRUCT(TJaegerTracerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TJaegerTracerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TJaegerTracer)
+
+class TBatchInfo
+{
+public:
+ TBatchInfo();
+ TBatchInfo(const TString& endpoint);
+
+ void PopFront();
+ void EmplaceBack(int size, NYT::TSharedRef&& value);
+ std::pair<i64, i64> DropQueue(int spanCount);
+ void IncrementTracesDropped(i64 delta);
+ std::tuple<std::vector<TSharedRef>, int, int> PeekQueue(const TJaegerTracerConfigPtr& config, std::optional<TSharedRef> processInfo);
+
+private:
+ std::deque<std::pair<int, TSharedRef>> BatchQueue_;
+
+ i64 QueueMemory_ = 0;
+ i64 QueueSize_ = 0;
+
+ NProfiling::TCounter TracesDequeued_;
+ NProfiling::TCounter TracesDropped_;
+ NProfiling::TGauge MemoryUsage_;
+ NProfiling::TGauge TraceQueueSize_;
+};
+
+class TJaegerChannelManager
+{
+public:
+ TJaegerChannelManager();
+ TJaegerChannelManager(const TIntrusivePtr<TJaegerTracerConfig>& config, const TString& endpoint);
+
+ bool Push(const std::vector<TSharedRef>& batches, int spanCount);
+ bool NeedsReopen(TInstant currentTime);
+ void ForceReset(TInstant currentTime);
+
+ TInstant GetReopenTime();
+
+private:
+ NRpc::IChannelPtr Channel_;
+
+ TString Endpoint_;
+
+ TInstant ReopenTime_;
+ TDuration RpcTimeout_;
+
+ NProfiling::TCounter PushedBytes_;
+ NProfiling::TCounter PushErrors_;
+ NProfiling::TSummary PayloadSize_;
+ NProfiling::TEventTimer PushDuration_;
+};
+
+class TJaegerTracer
+ : public ITracer
+{
+public:
+ TJaegerTracer(const TJaegerTracerConfigPtr& config);
+
+ TFuture<void> WaitFlush();
+
+ void Configure(const TJaegerTracerConfigPtr& config);
+
+ void Stop() override;
+
+ void Enqueue(TTraceContextPtr trace) override;
+
+private:
+ const NConcurrency::TActionQueuePtr ActionQueue_;
+ const NConcurrency::TPeriodicExecutorPtr FlushExecutor_;
+
+ TAtomicIntrusivePtr<TJaegerTracerConfig> Config_;
+
+ TMpscStack<TTraceContextPtr> TraceQueue_;
+
+ TInstant LastSuccessfullFlushTime_ = TInstant::Now();
+
+ THashMap<TString, TBatchInfo> BatchInfo_;
+ i64 TotalMemory_ = 0;
+ i64 TotalSize_ = 0;
+
+ TAtomicObject<TPromise<void>> QueueEmptyPromise_ = NewPromise<void>();
+
+ THashMap<TString, TJaegerChannelManager> CollectorChannels_;
+ NRpc::NGrpc::TChannelConfigPtr OpenChannelConfig_;
+
+ void Flush();
+ void DequeueAll(const TJaegerTracerConfigPtr& config);
+ void NotifyEmptyQueue();
+
+ std::tuple<std::vector<TSharedRef>, int, int> PeekQueue(const TJaegerTracerConfigPtr& config, const TString& endpoint);
+ void DropQueue(int batchCount, const TString& endpoint);
+ void DropFullQueue();
+
+ TSharedRef GetProcessInfo(const TJaegerTracerConfigPtr& config);
+};
+
+DEFINE_REFCOUNTED_TYPE(TJaegerTracer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/jaeger/ya.make b/yt/yt/library/tracing/jaeger/ya.make
new file mode 100644
index 0000000000..2276d20db2
--- /dev/null
+++ b/yt/yt/library/tracing/jaeger/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ yt/yt/library/tracing
+ yt/yt/core/rpc/grpc
+)
+
+SRCS(
+ model.proto
+ sampler.cpp
+ GLOBAL tracer.cpp
+)
+
+END()
diff --git a/yt/yt/library/tracing/public.cpp b/yt/yt/library/tracing/public.cpp
new file mode 100644
index 0000000000..4df9bcaa18
--- /dev/null
+++ b/yt/yt/library/tracing/public.cpp
@@ -0,0 +1 @@
+#include "public.h"
diff --git a/yt/yt/library/tracing/public.h b/yt/yt/library/tracing/public.h
new file mode 100644
index 0000000000..74635df631
--- /dev/null
+++ b/yt/yt/library/tracing/public.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <yt/yt/core/misc/ref_counted.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(ITracer)
+
+class TAsyncQueueTrace;
+class TAsyncQueueTraceGuard;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/tracer.cpp b/yt/yt/library/tracing/tracer.cpp
new file mode 100644
index 0000000000..346e5ef97b
--- /dev/null
+++ b/yt/yt/library/tracing/tracer.cpp
@@ -0,0 +1,10 @@
+#include "tracer.h"
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/tracer.h b/yt/yt/library/tracing/tracer.h
new file mode 100644
index 0000000000..8b53efd997
--- /dev/null
+++ b/yt/yt/library/tracing/tracer.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/ref_counted.h>
+
+#include <yt/yt/core/tracing/public.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ITracer
+ : public TRefCounted
+{
+ virtual void Enqueue(TTraceContextPtr trace) = 0;
+ virtual void Stop() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITracer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/unittests/sampler_ut.cpp b/yt/yt/library/tracing/unittests/sampler_ut.cpp
new file mode 100644
index 0000000000..cf67d95736
--- /dev/null
+++ b/yt/yt/library/tracing/unittests/sampler_ut.cpp
@@ -0,0 +1,39 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/tracing/jaeger/sampler.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+namespace NYT::NTracing {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSamplerTest, PerUser)
+{
+ auto config = New<TSamplerConfig>();
+ config->MinPerUserSamples = 1;
+ config->MinPerUserSamplesPeriod = TDuration::MilliSeconds(100);
+
+ auto sampler = New<TSampler>(config);
+
+ auto traceContext = TTraceContext::NewRoot("Test");
+
+ sampler->SampleTraceContext("prime", traceContext);
+ ASSERT_TRUE(traceContext->IsSampled());
+
+ traceContext->SetSampled(false);
+
+ sampler->SampleTraceContext("prime", traceContext);
+ ASSERT_FALSE(traceContext->IsSampled());
+
+ Sleep(TDuration::MilliSeconds(400));
+
+ sampler->SampleTraceContext("prime", traceContext);
+ ASSERT_TRUE(traceContext->IsSampled());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTracing
diff --git a/yt/yt/library/tracing/unittests/ya.make b/yt/yt/library/tracing/unittests/ya.make
new file mode 100644
index 0000000000..ab4acdbfbb
--- /dev/null
+++ b/yt/yt/library/tracing/unittests/ya.make
@@ -0,0 +1,13 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ sampler_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(yt/yt/library/tracing/jaeger)
+
+END()
diff --git a/yt/yt/library/tracing/ya.make b/yt/yt/library/tracing/ya.make
new file mode 100644
index 0000000000..793497e270
--- /dev/null
+++ b/yt/yt/library/tracing/ya.make
@@ -0,0 +1,34 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ public.cpp
+ tracer.cpp
+ async_queue_trace.cpp
+ batch_trace.cpp
+)
+
+PEERDIR(
+ # TODO(prime@:) remove this, once ADDINCL(yt) is removed.
+ yt/yt/build
+
+ # TODO(prime@:) remove this, once dependency cycle with yt/core is resolved
+ yt/yt_proto/yt/core
+)
+
+END()
+
+RECURSE(
+ jaeger
+ example
+ unittests
+)
+
+IF (NOT OPENSOURCE)
+ # NB: default-linux-x86_64-relwithdebinfo-opensource build does not support python programs and modules.
+ RECURSE(
+ integration
+ py
+ )
+ENDIF()
diff --git a/yt/yt/library/tvm/CMakeLists.darwin-x86_64.txt b/yt/yt/library/tvm/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..f92c150fd3
--- /dev/null
+++ b/yt/yt/library/tvm/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tvm)
+target_compile_options(yt-library-tvm PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-tvm PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(yt-library-tvm PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tvm/tvm_base.cpp
+)
diff --git a/yt/yt/library/tvm/CMakeLists.linux-aarch64.txt b/yt/yt/library/tvm/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..3a22559a59
--- /dev/null
+++ b/yt/yt/library/tvm/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tvm)
+target_compile_options(yt-library-tvm PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-tvm PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(yt-library-tvm PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tvm/tvm_base.cpp
+)
diff --git a/yt/yt/library/tvm/CMakeLists.linux-x86_64.txt b/yt/yt/library/tvm/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..3a22559a59
--- /dev/null
+++ b/yt/yt/library/tvm/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tvm)
+target_compile_options(yt-library-tvm PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-tvm PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(yt-library-tvm PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tvm/tvm_base.cpp
+)
diff --git a/yt/yt/library/tvm/CMakeLists.txt b/yt/yt/library/tvm/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/library/tvm/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/library/tvm/CMakeLists.windows-x86_64.txt b/yt/yt/library/tvm/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..5b92f509ce
--- /dev/null
+++ b/yt/yt/library/tvm/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-tvm)
+target_link_libraries(yt-library-tvm PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(yt-library-tvm PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/tvm/tvm_base.cpp
+)
diff --git a/yt/yt/library/tvm/public.h b/yt/yt/library/tvm/public.h
new file mode 100644
index 0000000000..eafaa2ecd7
--- /dev/null
+++ b/yt/yt/library/tvm/public.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <library/cpp/yt/memory/ref_counted.h>
+
+namespace NYT::NAuth {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(IServiceTicketAuth)
+DECLARE_REFCOUNTED_CLASS(TServiceTicketFixedAuth)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NAuth
diff --git a/yt/yt/library/tvm/tvm_base.cpp b/yt/yt/library/tvm/tvm_base.cpp
new file mode 100644
index 0000000000..a128f07a58
--- /dev/null
+++ b/yt/yt/library/tvm/tvm_base.cpp
@@ -0,0 +1,18 @@
+#include "tvm_base.h"
+
+namespace NYT::NAuth {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceTicketFixedAuth::TServiceTicketFixedAuth(TString ticket)
+ : Ticket_(std::move(ticket))
+{ }
+
+TString TServiceTicketFixedAuth::IssueServiceTicket()
+{
+ return Ticket_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NAuth
diff --git a/yt/yt/library/tvm/tvm_base.h b/yt/yt/library/tvm/tvm_base.h
new file mode 100644
index 0000000000..d4bbe95c23
--- /dev/null
+++ b/yt/yt/library/tvm/tvm_base.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/string.h>
+
+namespace NYT::NAuth {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IServiceTicketAuth
+ : public virtual TRefCounted
+{
+ virtual TString IssueServiceTicket() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IServiceTicketAuth)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceTicketFixedAuth
+ : public IServiceTicketAuth
+{
+public:
+ explicit TServiceTicketFixedAuth(TString ticket);
+
+ TString IssueServiceTicket() override;
+
+private:
+ const TString Ticket_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TServiceTicketFixedAuth)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NAuth
diff --git a/yt/yt/library/tvm/ya.make b/yt/yt/library/tvm/ya.make
new file mode 100644
index 0000000000..2f385cf2d4
--- /dev/null
+++ b/yt/yt/library/tvm/ya.make
@@ -0,0 +1,23 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ tvm_base.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/memory
+)
+
+IF(NOT OPENSOURCE)
+ SRCS(
+ tvm.cpp
+ )
+
+ PEERDIR(
+ library/cpp/tvmauth/client
+ )
+ENDIF()
+
+END()
diff --git a/yt/yt/library/undumpable/CMakeLists.darwin-x86_64.txt b/yt/yt/library/undumpable/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..d8b7f0de88
--- /dev/null
+++ b/yt/yt/library/undumpable/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,25 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-undumpable)
+target_compile_options(yt-library-undumpable PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-undumpable PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-threading
+ cpp-yt-memory
+ yt-library-profiling
+)
+target_sources(yt-library-undumpable PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/undumpable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/ref.cpp
+)
diff --git a/yt/yt/library/undumpable/CMakeLists.linux-aarch64.txt b/yt/yt/library/undumpable/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..89657ac801
--- /dev/null
+++ b/yt/yt/library/undumpable/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-undumpable)
+target_compile_options(yt-library-undumpable PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-undumpable PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-threading
+ cpp-yt-memory
+ yt-library-profiling
+)
+target_sources(yt-library-undumpable PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/undumpable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/ref.cpp
+)
diff --git a/yt/yt/library/undumpable/CMakeLists.linux-x86_64.txt b/yt/yt/library/undumpable/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..89657ac801
--- /dev/null
+++ b/yt/yt/library/undumpable/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,26 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-undumpable)
+target_compile_options(yt-library-undumpable PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(yt-library-undumpable PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-threading
+ cpp-yt-memory
+ yt-library-profiling
+)
+target_sources(yt-library-undumpable PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/undumpable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/ref.cpp
+)
diff --git a/yt/yt/library/undumpable/CMakeLists.txt b/yt/yt/library/undumpable/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/library/undumpable/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/library/undumpable/CMakeLists.windows-x86_64.txt b/yt/yt/library/undumpable/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..3b3e78325e
--- /dev/null
+++ b/yt/yt/library/undumpable/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(yt-library-undumpable)
+target_link_libraries(yt-library-undumpable PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-assert
+ cpp-yt-threading
+ cpp-yt-memory
+ yt-library-profiling
+)
+target_sources(yt-library-undumpable PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/undumpable.cpp
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/undumpable/ref.cpp
+)
diff --git a/yt/yt/library/undumpable/README.md b/yt/yt/library/undumpable/README.md
new file mode 100644
index 0000000000..e6bed60c59
--- /dev/null
+++ b/yt/yt/library/undumpable/README.md
@@ -0,0 +1,9 @@
+# undumpable
+
+Библиотека undumpable позволяет исключить из core-dump-а некоторые участки памяти.
+
+Линукс позволяет исключать память из coredump с помощью системного вызова madvice(DONTNEED).
+Но этот системный вызов слишком дорог, чтобы делать его на каждую аллокацию.
+
+Вместо этого, мы поддерживаем в памяти список undumpable участков, и размечаем их через madvise(DONTNEED)
+в момент креша, из обработчика сигнала.
diff --git a/yt/yt/library/undumpable/ref.cpp b/yt/yt/library/undumpable/ref.cpp
new file mode 100644
index 0000000000..24602efe22
--- /dev/null
+++ b/yt/yt/library/undumpable/ref.cpp
@@ -0,0 +1,49 @@
+#include "ref.h"
+
+#include "undumpable.h"
+
+#include <util/generic/size_literals.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr size_t MinUndumpableSize = 64_KB;
+
+struct TUndumpableHolder
+ : public TSharedRangeHolder
+{
+ explicit TUndumpableHolder(const TSharedRef& ref)
+ : Underlying(ref.GetHolder())
+ , Mark(MarkUndumpable(const_cast<char*>(ref.Begin()), ref.Size()))
+ { }
+
+ ~TUndumpableHolder()
+ {
+ if (Mark) {
+ UnmarkUndumpable(Mark);
+ }
+ }
+
+ // TSharedRangeHolder overrides.
+ std::optional<size_t> GetTotalByteSize() const override
+ {
+ return Underlying->GetTotalByteSize();
+ }
+
+ const TSharedRangeHolderPtr Underlying;
+ TUndumpableMark* const Mark;
+};
+
+TSharedRef MarkUndumpable(const TSharedRef& ref)
+{
+ if (ref.Size() >= MinUndumpableSize) {
+ return TSharedRef(ref, New<TUndumpableHolder>(ref));
+ } else {
+ return ref;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/library/undumpable/ref.h b/yt/yt/library/undumpable/ref.h
new file mode 100644
index 0000000000..bdc194271d
--- /dev/null
+++ b/yt/yt/library/undumpable/ref.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef MarkUndumpable(const TSharedRef& ref);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/library/undumpable/undumpable.cpp b/yt/yt/library/undumpable/undumpable.cpp
new file mode 100644
index 0000000000..c5d8191b4f
--- /dev/null
+++ b/yt/yt/library/undumpable/undumpable.cpp
@@ -0,0 +1,225 @@
+#include "undumpable.h"
+
+#if defined(_linux_)
+#include <sys/mman.h>
+#endif
+
+#include <util/generic/hash.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/memory/ref_counted.h>
+#include <library/cpp/yt/memory/new.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <atomic>
+#include <optional>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TUndumpableMark
+{
+ // TUndumpableMark-s are never freed. All objects are linked through NextMark.
+ TUndumpableMark* NextMark = nullptr;
+ TUndumpableMark* NextFree = nullptr;
+
+ void* Ptr = nullptr;
+ size_t Size = 0;
+};
+
+class TUndumpableMemoryManager
+{
+public:
+ constexpr TUndumpableMemoryManager() = default;
+
+ TUndumpableMark* MarkUndumpable(void* ptr, size_t size)
+ {
+ UndumpableSize_.fetch_add(size, std::memory_order::relaxed);
+
+ auto guard = Guard(MarkListsLock_);
+ auto* mark = GetFreeMark();
+ mark->Ptr = ptr;
+ mark->Size = size;
+ return mark;
+ }
+
+ void UnmarkUndumpable(TUndumpableMark* mark)
+ {
+ UndumpableSize_.fetch_sub(mark->Size, std::memory_order::relaxed);
+
+ mark->Size = 0;
+ mark->Ptr = nullptr;
+
+ auto guard = Guard(MarkListsLock_);
+ FreeMark(mark);
+ }
+
+ void MarkUndumpableOob(void* ptr, size_t size)
+ {
+ auto* mark = MarkUndumpable(ptr, size);
+ auto guard = Guard(MarkTableLock_);
+
+ if (!MarkTable_) {
+ MarkTable_.emplace();
+ }
+ YT_VERIFY(MarkTable_->emplace(ptr, mark).second);
+ }
+
+ void UnmarkUndumpableOob(void* ptr)
+ {
+ auto guard = Guard(MarkTableLock_);
+
+ if (!MarkTable_) {
+ MarkTable_.emplace();
+ }
+
+ auto it = MarkTable_->find(ptr);
+ YT_VERIFY(it != MarkTable_->end());
+ auto* mark = it->second;
+ MarkTable_->erase(it);
+ guard.Release();
+
+ UnmarkUndumpable(mark);
+ }
+
+ size_t GetUndumpableMemorySize() const
+ {
+ return UndumpableSize_.load();
+ }
+
+ size_t GetUndumpableMemoryFootprint() const
+ {
+ return UndumpableFootprint_.load();
+ }
+
+ TCutBlocksInfo CutUndumpableRegionsFromCoredump()
+ {
+ TCutBlocksInfo result;
+#if defined(_linux_)
+ auto* mark = AllMarksHead_;
+ while (mark) {
+ int ret = ::madvise(mark->Ptr, mark->Size, MADV_DONTDUMP);
+ if (ret == 0) {
+ ++result.MarkedSize += mark->Size;
+ } else {
+ auto errorCode = LastSystemError();
+ for (auto& record : result.FailedToMarkMemory) {
+ if (record.ErrorCode == 0 || record.ErrorCode == errorCode) {
+ record.ErrorCode = errorCode;
+ record.Size += mark->Size;
+ break;
+ }
+ }
+ }
+ mark = mark->NextMark;
+ }
+#endif
+ return result;
+ }
+
+private:
+ std::atomic<size_t> UndumpableSize_ = 0;
+ std::atomic<size_t> UndumpableFootprint_ = 0;
+
+ NThreading::TSpinLock MarkListsLock_;
+ TUndumpableMark* AllMarksHead_ = nullptr;
+ TUndumpableMark* FreeMarksHead_ = nullptr;
+
+ NThreading::TSpinLock MarkTableLock_;
+ std::optional<THashMap<void*, TUndumpableMark*>> MarkTable_;
+
+ TUndumpableMark* GetFreeMark()
+ {
+ if (FreeMarksHead_) {
+ auto mark = FreeMarksHead_;
+ FreeMarksHead_ = mark->NextFree;
+ return mark;
+ }
+
+ auto* mark = new TUndumpableMark();
+ UndumpableFootprint_.fetch_add(sizeof(*mark), std::memory_order::relaxed);
+
+ mark->NextMark = AllMarksHead_;
+ AllMarksHead_ = mark;
+ return mark;
+ }
+
+ void FreeMark(TUndumpableMark* mark)
+ {
+ mark->NextFree = FreeMarksHead_;
+ FreeMarksHead_ = mark;
+ }
+};
+
+static constinit TUndumpableMemoryManager UndumpableMemoryManager;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUndumpableMark* MarkUndumpable(void* ptr, size_t size)
+{
+ return UndumpableMemoryManager.MarkUndumpable(ptr, size);
+}
+
+void UnmarkUndumpable(TUndumpableMark* mark)
+{
+ UndumpableMemoryManager.UnmarkUndumpable(mark);
+}
+
+void MarkUndumpableOob(void* ptr, size_t size)
+{
+ UndumpableMemoryManager.MarkUndumpableOob(ptr, size);
+}
+
+void UnmarkUndumpableOob(void* ptr)
+{
+ UndumpableMemoryManager.UnmarkUndumpableOob(ptr);
+}
+
+size_t GetUndumpableMemorySize()
+{
+ return UndumpableMemoryManager.GetUndumpableMemorySize();
+}
+
+size_t GetUndumpableMemoryFootprint()
+{
+ return UndumpableMemoryManager.GetUndumpableMemoryFootprint();
+}
+
+TCutBlocksInfo CutUndumpableRegionsFromCoredump()
+{
+ return UndumpableMemoryManager.CutUndumpableRegionsFromCoredump();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TUndumpableSensors)
+
+struct TUndumpableSensors
+ : public TRefCounted
+{
+ TUndumpableSensors()
+ {
+ NProfiling::TProfiler profiler{"/memory"};
+
+ profiler.AddFuncGauge("/undumpable_memory_size", MakeStrong(this), [] {
+ return GetUndumpableMemorySize();
+ });
+
+ profiler.AddFuncGauge("/undumpable_memory_footprint", MakeStrong(this), [] {
+ return GetUndumpableMemoryFootprint();
+ });
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TUndumpableSensors)
+
+static const TUndumpableSensorsPtr Sensors = New<TUndumpableSensors>();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/library/undumpable/undumpable.h b/yt/yt/library/undumpable/undumpable.h
new file mode 100644
index 0000000000..8a74c3f134
--- /dev/null
+++ b/yt/yt/library/undumpable/undumpable.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <util/system/types.h>
+
+#include <array>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TUndumpableMark;
+
+//! Adds byte range to undumpable set.
+TUndumpableMark* MarkUndumpable(void* ptr, size_t size);
+
+//! Removes byte range from undumpable set.
+void UnmarkUndumpable(TUndumpableMark* mark);
+
+//! Add byte range to undumpable set.
+/**
+ * Unlike MarkUndumpable, this method does not require user to keep pointer to TUndumpableMark.
+ */
+void MarkUndumpableOob(void* ptr, size_t size);
+
+//! Remove byte range from undumpable set.
+void UnmarkUndumpableOob(void* ptr);
+
+//! Returns the total size of undumpable set.
+size_t GetUndumpableMemorySize();
+
+//! Returns the estimate of memory consumed by internal data structures.
+size_t GetUndumpableMemoryFootprint();
+
+struct TCutBlocksInfo
+{
+ struct TFailedInfo
+ {
+ int ErrorCode = 0;
+ size_t Size = 0;
+ };
+
+ size_t MarkedSize = 0;
+
+ static constexpr int MaxFailedRecordsCount = 8;
+ std::array<TFailedInfo, MaxFailedRecordsCount> FailedToMarkMemory{};
+};
+
+//! CutUndumpableFromCoredump call's madvice(MADV_DONTNEED) for all current undumpable objects.
+/**
+ * This function is async-signal safe. Usually, this function should be called from segfault handler.
+ */
+TCutBlocksInfo CutUndumpableRegionsFromCoredump();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/library/undumpable/unittests/undumpable_ut.cpp b/yt/yt/library/undumpable/unittests/undumpable_ut.cpp
new file mode 100644
index 0000000000..8033ebad46
--- /dev/null
+++ b/yt/yt/library/undumpable/unittests/undumpable_ut.cpp
@@ -0,0 +1,61 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/library/undumpable/undumpable.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(UndumpableMemory, Mark)
+{
+ std::vector<int> bigVector;
+ bigVector.resize(1024 * 1024);
+
+ auto mark = MarkUndumpable(&bigVector[0], bigVector.size() * sizeof(bigVector[0]));
+ ASSERT_GT(GetUndumpableMemorySize(), 0u);
+ ASSERT_GT(GetUndumpableMemoryFootprint(), 0u);
+
+ CutUndumpableRegionsFromCoredump();
+
+ UnmarkUndumpable(mark);
+
+ ASSERT_EQ(GetUndumpableMemorySize(), 0u);
+ ASSERT_GT(GetUndumpableMemoryFootprint(), 0u);
+}
+
+TEST(UndumpableMemory, MarkOOB)
+{
+ std::vector<int> bigVector;
+ bigVector.resize(1024 * 1024);
+
+ MarkUndumpableOob(&bigVector[0], bigVector.size() * sizeof(bigVector[0]));
+ ASSERT_GT(GetUndumpableMemorySize(), 0u);
+ ASSERT_GT(GetUndumpableMemoryFootprint(), 0u);
+
+ CutUndumpableRegionsFromCoredump();
+
+ UnmarkUndumpableOob(&bigVector[0]);
+
+ ASSERT_EQ(GetUndumpableMemorySize(), 0u);
+ ASSERT_GT(GetUndumpableMemoryFootprint(), 0u);
+}
+
+TEST(UndumpableMemory, UnalignedSize)
+{
+ std::vector<int> bigVector;
+ bigVector.resize(1024 * 1024 + 43);
+
+ auto mark = MarkUndumpable(&bigVector[0], bigVector.size() * sizeof(bigVector[0]));
+ ASSERT_GT(GetUndumpableMemorySize(), 0u);
+ ASSERT_GT(GetUndumpableMemoryFootprint(), 0u);
+
+ CutUndumpableRegionsFromCoredump();
+
+ UnmarkUndumpable(mark);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/library/undumpable/unittests/ya.make b/yt/yt/library/undumpable/unittests/ya.make
new file mode 100644
index 0000000000..ef64d1488d
--- /dev/null
+++ b/yt/yt/library/undumpable/unittests/ya.make
@@ -0,0 +1,19 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (OS_LINUX)
+ ALLOCATOR(TCMALLOC_256K)
+ENDIF()
+
+SRCS(
+ undumpable_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/library/undumpable
+)
+
+END()
diff --git a/yt/yt/library/undumpable/ya.make b/yt/yt/library/undumpable/ya.make
new file mode 100644
index 0000000000..724c504e84
--- /dev/null
+++ b/yt/yt/library/undumpable/ya.make
@@ -0,0 +1,21 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ undumpable.cpp
+ ref.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/assert
+ library/cpp/yt/threading
+ library/cpp/yt/memory
+
+ yt/yt/library/profiling
+)
+
+END()
+
+RECURSE(unittests)
+
diff --git a/yt/yt/library/ytprof/CMakeLists.txt b/yt/yt/library/ytprof/CMakeLists.txt
new file mode 100644
index 0000000000..dbfb934bae
--- /dev/null
+++ b/yt/yt/library/ytprof/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(api)
diff --git a/yt/yt/library/ytprof/api/CMakeLists.darwin-x86_64.txt b/yt/yt/library/ytprof/api/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..1a27698386
--- /dev/null
+++ b/yt/yt/library/ytprof/api/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,21 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-ytprof-api)
+target_compile_options(library-ytprof-api PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(library-ytprof-api PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(library-ytprof-api PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/ytprof/api/api.cpp
+)
diff --git a/yt/yt/library/ytprof/api/CMakeLists.linux-aarch64.txt b/yt/yt/library/ytprof/api/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..eda68274b6
--- /dev/null
+++ b/yt/yt/library/ytprof/api/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-ytprof-api)
+target_compile_options(library-ytprof-api PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(library-ytprof-api PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(library-ytprof-api PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/ytprof/api/api.cpp
+)
diff --git a/yt/yt/library/ytprof/api/CMakeLists.linux-x86_64.txt b/yt/yt/library/ytprof/api/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..eda68274b6
--- /dev/null
+++ b/yt/yt/library/ytprof/api/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,22 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-ytprof-api)
+target_compile_options(library-ytprof-api PRIVATE
+ -Wdeprecated-this-capture
+)
+target_link_libraries(library-ytprof-api PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(library-ytprof-api PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/ytprof/api/api.cpp
+)
diff --git a/yt/yt/library/ytprof/api/CMakeLists.txt b/yt/yt/library/ytprof/api/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt/library/ytprof/api/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt/library/ytprof/api/CMakeLists.windows-x86_64.txt b/yt/yt/library/ytprof/api/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..c8f4bfbb7c
--- /dev/null
+++ b/yt/yt/library/ytprof/api/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,18 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(library-ytprof-api)
+target_link_libraries(library-ytprof-api PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-yt-memory
+)
+target_sources(library-ytprof-api PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt/library/ytprof/api/api.cpp
+)
diff --git a/yt/yt/library/ytprof/api/api.cpp b/yt/yt/library/ytprof/api/api.cpp
new file mode 100644
index 0000000000..debee0630c
--- /dev/null
+++ b/yt/yt/library/ytprof/api/api.cpp
@@ -0,0 +1,86 @@
+#include "api.h"
+
+namespace NYT::NYTProf {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_REFCOUNTED_TYPE(TProfilerTag)
+
+struct TCpuProfilerTags;
+
+// This variable is referenced from signal handler.
+constinit thread_local std::atomic<TCpuProfilerTags*> CpuProfilerTagsPtr = nullptr;
+
+struct TCpuProfilerTags
+{
+ TCpuProfilerTags()
+ {
+ CpuProfilerTagsPtr = this;
+ }
+
+ ~TCpuProfilerTags()
+ {
+ CpuProfilerTagsPtr = nullptr;
+ }
+
+ std::array<TAtomicSignalPtr<TProfilerTag>, MaxActiveTags> Tags;
+};
+
+// We can't reference CpuProfilerTags from signal handler,
+// since it may trigger lazy initialization.
+thread_local TCpuProfilerTags CpuProfilerTags;
+
+std::array<TAtomicSignalPtr<TProfilerTag>, MaxActiveTags>* GetCpuProfilerTags()
+{
+ auto tags = CpuProfilerTagsPtr.load();
+ if (tags) {
+ return &(tags->Tags);
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCpuProfilerTagGuard::TCpuProfilerTagGuard(TProfilerTagPtr tag)
+{
+ for (int i = 0; i < MaxActiveTags; i++) {
+ if (!CpuProfilerTags.Tags[i].IsSetFromThread()) {
+ CpuProfilerTags.Tags[i].StoreFromThread(std::move(tag));
+ TagIndex_ = i;
+ return;
+ }
+ }
+}
+
+TCpuProfilerTagGuard::~TCpuProfilerTagGuard()
+{
+ if (TagIndex_ != -1) {
+ CpuProfilerTags.Tags[TagIndex_].StoreFromThread(nullptr);
+ }
+}
+
+TCpuProfilerTagGuard::TCpuProfilerTagGuard(TCpuProfilerTagGuard&& other)
+ : TagIndex_(other.TagIndex_)
+{
+ other.TagIndex_ = -1;
+}
+
+TCpuProfilerTagGuard& TCpuProfilerTagGuard::operator = (TCpuProfilerTagGuard&& other)
+{
+ if (this == &other) {
+ return *this;
+ }
+
+ if (TagIndex_ != -1) {
+ CpuProfilerTags.Tags[TagIndex_].StoreFromThread(nullptr);
+ }
+
+ TagIndex_ = other.TagIndex_;
+ other.TagIndex_ = -1;
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTProf
diff --git a/yt/yt/library/ytprof/api/api.h b/yt/yt/library/ytprof/api/api.h
new file mode 100644
index 0000000000..322c0f951c
--- /dev/null
+++ b/yt/yt/library/ytprof/api/api.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <array>
+#include <variant>
+#include <optional>
+
+#include <yt/yt/library/ytprof/api/atomic_signal_ptr.h>
+
+#include <library/cpp/yt/cpu_clock/public.h>
+#include <library/cpp/yt/memory/intrusive_ptr.h>
+
+namespace NYT::NYTProf {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProfilerTag final
+{
+ TString Name;
+ std::optional<TString> StringValue;
+ std::optional<i64> IntValue;
+
+ TProfilerTag(const TString& name, const TString& value)
+ : Name(name)
+ , StringValue(value)
+ { }
+
+ TProfilerTag(const TString& name, i64 value)
+ : Name(name)
+ , IntValue(value)
+ { }
+};
+
+typedef TIntrusivePtr<TProfilerTag> TProfilerTagPtr;
+
+constexpr int MaxActiveTags = 4;
+
+std::array<TAtomicSignalPtr<TProfilerTag>, MaxActiveTags>* GetCpuProfilerTags();
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Hooks for yt/yt/core fibers.
+void* AcquireFiberTagStorage();
+std::vector<std::pair<TString, std::variant<TString, i64>>> ReadFiberTags(void* storage);
+void ReleaseFiberTagStorage(void* storage);
+TCpuInstant GetTraceContextTimingCheckpoint();
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCpuProfilerTagGuard
+{
+public:
+ TCpuProfilerTagGuard() = default;
+
+ explicit TCpuProfilerTagGuard(TProfilerTagPtr tag);
+ ~TCpuProfilerTagGuard();
+
+ TCpuProfilerTagGuard(TCpuProfilerTagGuard&& other);
+ TCpuProfilerTagGuard(const TCpuProfilerTagGuard& other) = delete;
+
+ TCpuProfilerTagGuard& operator = (TCpuProfilerTagGuard&& other);
+ TCpuProfilerTagGuard& operator = (const TCpuProfilerTagGuard& other) = delete;
+
+private:
+ int TagIndex_ = -1;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTProf
diff --git a/yt/yt/library/ytprof/api/atomic_signal_ptr.h b/yt/yt/library/ytprof/api/atomic_signal_ptr.h
new file mode 100644
index 0000000000..e0dece904c
--- /dev/null
+++ b/yt/yt/library/ytprof/api/atomic_signal_ptr.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <library/cpp/yt/memory/intrusive_ptr.h>
+#include <library/cpp/yt/memory/ref_counted.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! TAtomicSignalPtr is special kind of reference, that is safe to use from signal handler.
+template <class T>
+class TAtomicSignalPtr
+{
+public:
+ constexpr TAtomicSignalPtr() noexcept = default;
+
+ TAtomicSignalPtr(const TAtomicSignalPtr& other) = delete;
+
+ TIntrusivePtr<T> GetFromSignal() const
+ {
+ return TIntrusivePtr<T>(T_);
+ }
+
+ void StoreFromThread(const TIntrusivePtr<T>& ptr)
+ {
+ if (T_) {
+ auto tmp = T_;
+
+ T_ = nullptr;
+ std::atomic_signal_fence(std::memory_order::seq_cst);
+
+ Unref(tmp);
+ }
+
+ if (ptr) {
+ T_ = ptr.Get();
+ Ref(T_);
+ }
+ }
+
+ bool IsSetFromThread() const
+ {
+ return T_ != nullptr;
+ }
+
+ ~TAtomicSignalPtr()
+ {
+ StoreFromThread(nullptr);
+ }
+
+private:
+ T* T_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT
diff --git a/yt/yt/library/ytprof/api/ya.make b/yt/yt/library/ytprof/api/ya.make
new file mode 100644
index 0000000000..a63f1cb9f1
--- /dev/null
+++ b/yt/yt/library/ytprof/api/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ api.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/memory
+)
+
+END()
+
diff --git a/yt/yt/ya_check_dependencies.inc b/yt/yt/ya_check_dependencies.inc
new file mode 100644
index 0000000000..c628b82cf3
--- /dev/null
+++ b/yt/yt/ya_check_dependencies.inc
@@ -0,0 +1,15 @@
+INCLUDE_ONCE(yes)
+
+CHECK_DEPENDENT_DIRS(ALLOW_ONLY ALL
+ build
+ certs
+ contrib
+ devtools
+ infra
+ library
+ mapreduce
+ tools
+ util
+ yt
+)
+
diff --git a/yt/yt_proto/CMakeLists.txt b/yt/yt_proto/CMakeLists.txt
new file mode 100644
index 0000000000..ef64c4e308
--- /dev/null
+++ b/yt/yt_proto/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(yt)
diff --git a/yt/yt_proto/yt/CMakeLists.txt b/yt/yt_proto/yt/CMakeLists.txt
new file mode 100644
index 0000000000..d8f6bf9771
--- /dev/null
+++ b/yt/yt_proto/yt/CMakeLists.txt
@@ -0,0 +1,10 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+add_subdirectory(core)
+add_subdirectory(formats)
diff --git a/yt/yt_proto/yt/core/CMakeLists.darwin-x86_64.txt b/yt/yt_proto/yt/core/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..f131b5c4e9
--- /dev/null
+++ b/yt/yt_proto/yt/core/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,190 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-core)
+target_include_directories(yt_proto-yt-core PUBLIC
+ ${CMAKE_BINARY_DIR}/yt
+)
+target_link_libraries(yt_proto-yt-core PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/crypto/proto/crypto.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/bloom_filter.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/error.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/guid.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/span.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/bus/proto/bus.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/rpc/proto/rpc.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/attributes.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/ypath.proto
+)
+target_proto_addincls(yt_proto-yt-core
+ ./yt
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-core
+ --cpp_out=${CMAKE_BINARY_DIR}/yt
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/yt
+)
diff --git a/yt/yt_proto/yt/core/CMakeLists.linux-aarch64.txt b/yt/yt_proto/yt/core/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..2907cf3959
--- /dev/null
+++ b/yt/yt_proto/yt/core/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,191 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-core)
+target_include_directories(yt_proto-yt-core PUBLIC
+ ${CMAKE_BINARY_DIR}/yt
+)
+target_link_libraries(yt_proto-yt-core PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/crypto/proto/crypto.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/bloom_filter.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/error.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/guid.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/span.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/bus/proto/bus.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/rpc/proto/rpc.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/attributes.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/ypath.proto
+)
+target_proto_addincls(yt_proto-yt-core
+ ./yt
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-core
+ --cpp_out=${CMAKE_BINARY_DIR}/yt
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/yt
+)
diff --git a/yt/yt_proto/yt/core/CMakeLists.linux-x86_64.txt b/yt/yt_proto/yt/core/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..2907cf3959
--- /dev/null
+++ b/yt/yt_proto/yt/core/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,191 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-core)
+target_include_directories(yt_proto-yt-core PUBLIC
+ ${CMAKE_BINARY_DIR}/yt
+)
+target_link_libraries(yt_proto-yt-core PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/crypto/proto/crypto.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/bloom_filter.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/error.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/guid.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/span.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/bus/proto/bus.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/rpc/proto/rpc.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/attributes.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/ypath.proto
+)
+target_proto_addincls(yt_proto-yt-core
+ ./yt
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-core
+ --cpp_out=${CMAKE_BINARY_DIR}/yt
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/yt
+)
diff --git a/yt/yt_proto/yt/core/CMakeLists.txt b/yt/yt_proto/yt/core/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt_proto/yt/core/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt_proto/yt/core/CMakeLists.windows-x86_64.txt b/yt/yt_proto/yt/core/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..f131b5c4e9
--- /dev/null
+++ b/yt/yt_proto/yt/core/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,190 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-core)
+target_include_directories(yt_proto-yt-core PUBLIC
+ ${CMAKE_BINARY_DIR}/yt
+)
+target_link_libraries(yt_proto-yt-core PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-core PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/crypto/proto/crypto.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/bloom_filter.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/error.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/guid.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/span.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/bus/proto/bus.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/rpc/proto/rpc.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/attributes.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/core/ytree/proto/ypath.proto
+)
+target_proto_addincls(yt_proto-yt-core
+ ./yt
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/yt
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-core
+ --cpp_out=${CMAKE_BINARY_DIR}/yt
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/yt
+)
diff --git a/yt/yt_proto/yt/core/bus/proto/bus.proto b/yt/yt_proto/yt/core/bus/proto/bus.proto
new file mode 100644
index 0000000000..daf9d5f083
--- /dev/null
+++ b/yt/yt_proto/yt/core/bus/proto/bus.proto
@@ -0,0 +1,13 @@
+package NYT.NBus.NProto;
+
+import "yt_proto/yt/core/misc/proto/guid.proto";
+
+message THandshake
+{
+ required NYT.NProto.TGuid foreign_connection_id = 1;
+
+ // Only passed from client to server.
+ optional int32 multiplexing_band = 2; // EMultiplexingBand
+
+ optional int32 encryption_mode = 3; // EEncryptionMode
+}
diff --git a/yt/yt_proto/yt/core/crypto/proto/crypto.proto b/yt/yt_proto/yt/core/crypto/proto/crypto.proto
new file mode 100644
index 0000000000..3534fc93d1
--- /dev/null
+++ b/yt/yt_proto/yt/core/crypto/proto/crypto.proto
@@ -0,0 +1,11 @@
+package NYT.NCrypto.NProto;
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TMD5Hasher
+{
+ required bytes state = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt_proto/yt/core/misc/proto/bloom_filter.proto b/yt/yt_proto/yt/core/misc/proto/bloom_filter.proto
new file mode 100644
index 0000000000..b950a6cec7
--- /dev/null
+++ b/yt/yt_proto/yt/core/misc/proto/bloom_filter.proto
@@ -0,0 +1,10 @@
+package NYT.NProto;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/misc";
+
+message TBloomFilter
+{
+ required int32 version = 1;
+ required bytes bitmap = 2;
+ required int32 hash_count = 3;
+}
diff --git a/yt/yt_proto/yt/core/misc/proto/error.proto b/yt/yt_proto/yt/core/misc/proto/error.proto
new file mode 100644
index 0000000000..2e20bc0269
--- /dev/null
+++ b/yt/yt_proto/yt/core/misc/proto/error.proto
@@ -0,0 +1,20 @@
+package NYT.NProto;
+
+option java_package = "tech.ytsaurus";
+option java_multiple_files = true;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/misc";
+
+import "yt_proto/yt/core/ytree/proto/attributes.proto";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TError
+{
+ required int32 code = 1;
+ optional string message = 2;
+ optional NYT.NYTree.NProto.TAttributeDictionary attributes = 3;
+ repeated TError inner_errors = 4;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt_proto/yt/core/misc/proto/guid.proto b/yt/yt_proto/yt/core/misc/proto/guid.proto
new file mode 100644
index 0000000000..61f191bf24
--- /dev/null
+++ b/yt/yt_proto/yt/core/misc/proto/guid.proto
@@ -0,0 +1,17 @@
+package NYT.NProto;
+
+option java_package = "tech.ytsaurus";
+option java_multiple_files = true;
+
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/misc";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TGuid
+{
+ required fixed64 first = 1;
+ required fixed64 second = 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto b/yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto
new file mode 100644
index 0000000000..79b4f242a3
--- /dev/null
+++ b/yt/yt_proto/yt/core/misc/proto/protobuf_helpers.proto
@@ -0,0 +1,28 @@
+package NYT.NProto;
+
+option java_package = "tech.ytsaurus";
+option java_multiple_files = true;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/misc";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TSerializedMessageEnvelope
+{
+ optional int32 codec = 1 [default = 0];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TExtension
+{
+ required int32 tag = 1;
+ required bytes data = 2;
+}
+
+message TExtensionSet
+{
+ repeated TExtension extensions = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt_proto/yt/core/rpc/proto/rpc.proto b/yt/yt_proto/yt/core/rpc/proto/rpc.proto
new file mode 100644
index 0000000000..852bc3cc79
--- /dev/null
+++ b/yt/yt_proto/yt/core/rpc/proto/rpc.proto
@@ -0,0 +1,216 @@
+package NYT.NRpc.NProto;
+
+option java_package = "tech.ytsaurus.rpc";
+option java_multiple_files = true;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/rpc";
+
+
+import "yt_proto/yt/core/misc/proto/guid.proto";
+import "yt_proto/yt/core/misc/proto/error.proto";
+import "yt_proto/yt/core/tracing/proto/tracing_ext.proto";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TStreamingParameters
+{
+ optional int64 window_size = 1;
+ optional int64 read_timeout = 2;
+ optional int64 write_timeout = 3;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TRequestHeader
+{
+ optional NYT.NProto.TGuid request_id = 1;
+ required string service = 2;
+ required string method = 3;
+ optional NYT.NProto.TGuid realm_id = 8;
+
+ optional int32 protocol_version_major = 9;
+ optional int32 protocol_version_minor = 19;
+
+ optional int64 start_time = 7;
+ optional bool retry = 11;
+ optional NYT.NProto.TGuid mutation_id = 13;
+
+ optional int64 timeout = 10;
+
+ optional string user = 12;
+ optional string user_tag = 25;
+
+ optional int32 tos_level = 14;
+
+ optional int32 request_format = 15; // EMessageFormat
+ optional int32 response_format = 16; // EMessageFormat
+
+ optional bool uncancelable = 17;
+
+ optional string user_agent = 18;
+
+ optional int64 logging_suppression_timeout = 20;
+ optional bool disable_logging_suppression_if_request_failed = 21;
+
+ // COMPAT(kiseloyvp): these are missing when legacy mode is used
+ optional int32 request_codec = 23; // ECodec
+ optional int32 response_codec = 24; // ECodec
+
+ optional TStreamingParameters server_attachments_streaming_parameters = 33;
+
+ optional bytes request_format_options = 34; // TYsonString
+ optional bytes response_format_options = 35; // TYsonString
+
+ repeated int32 declared_client_feature_ids = 36;
+ repeated int32 required_server_feature_ids = 37;
+
+ optional int64 logical_request_weight = 38;
+
+ reserved 32;
+
+ /*
+ * Extensions:
+ * transaction_id 100
+ * suppress_access_tracking 103
+ * ypath_header_ext 104
+ * caching_header_ext 105
+ * tracing_ext 106
+ * suppress_modification_tracking 108
+ * prerequisites_ext 109
+ * credentials_ext 110
+ * balancing_ext 111
+ * multicell_sync_ext 112
+ * suppress_expiration_timeout_renewal 113
+ * workload_descriptor 114
+ * ssl_credentials_ext 120
+ * req_execute_ext 200
+ * req_multiread_ext 201
+ */
+
+ extensions 100 to max;
+
+ extend NRpc.NProto.TRequestHeader
+ {
+ optional NYT.NTracing.NProto.TTracingExt tracing_ext = 106;
+ }
+}
+
+message TBalancingExt
+{
+ extend NRpc.NProto.TRequestHeader
+ {
+ optional TBalancingExt balancing_ext = 111;
+ }
+
+ optional bool enable_stickiness = 1;
+ optional int32 sticky_group_size = 2 [default = 1];
+ optional bool enable_client_stickiness = 3;
+}
+
+message TCredentialsExt
+{
+ extend NRpc.NProto.TRequestHeader
+ {
+ optional TCredentialsExt credentials_ext = 110;
+ }
+
+ reserved 1;
+ optional string token = 2;
+ optional string session_id = 4;
+ optional string ssl_session_id = 5;
+ optional string user_ticket = 6;
+ optional string service_ticket = 7;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TResponseHeader
+{
+ optional NYT.NProto.TGuid request_id = 1;
+
+ // If omitted then OK is assumed.
+ optional NYT.NProto.TError error = 2;
+
+ optional int32 format = 3; // EMessageFormat
+
+ // COMPAT(kiseloyvp): this is missing when legacy mode is used
+ optional int32 codec = 6; // ECodec
+
+ reserved 5;
+ extensions 100 to max;
+}
+
+message THedgingExt
+{
+ extend NRpc.NProto.TResponseHeader
+ {
+ optional THedgingExt hedging_ext = 100;
+ }
+
+ optional bool backup_responded = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TRequestCancelationHeader
+{
+ required NYT.NProto.TGuid request_id = 1;
+ required string service = 2;
+ required string method = 3;
+ optional NYT.NProto.TGuid realm_id = 4;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TStreamingPayloadHeader
+{
+ required NYT.NProto.TGuid request_id = 1;
+ required string service = 2;
+ required string method = 3;
+ optional NYT.NProto.TGuid realm_id = 4;
+ required int32 sequence_number = 5;
+ optional int32 codec = 6; // ECodec
+
+ reserved 7;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TStreamingFeedbackHeader
+{
+ required NYT.NProto.TGuid request_id = 1;
+ required string service = 2;
+ required string method = 3;
+ optional NYT.NProto.TGuid realm_id = 4;
+ required int64 read_position = 5;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqDiscover
+{
+ optional int64 reply_delay = 1;
+
+ extensions 100 to max;
+}
+
+message TRspDiscover
+{
+ required bool up = 1;
+ repeated string suggested_addresses = 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum EWriterFeedback
+{
+ WF_HANDSHAKE = 0;
+ WF_SUCCESS = 1;
+}
+
+message TWriterFeedback
+{
+ required EWriterFeedback feedback = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt_proto/yt/core/tracing/proto/span.proto b/yt/yt_proto/yt/core/tracing/proto/span.proto
new file mode 100644
index 0000000000..9d08cc47a6
--- /dev/null
+++ b/yt/yt_proto/yt/core/tracing/proto/span.proto
@@ -0,0 +1,38 @@
+package NYT.NTracing.NProto;
+
+import "yt_proto/yt/core/misc/proto/guid.proto";
+
+option java_package = "tech.ytsaurus.tracing";
+option java_multiple_files = true;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/tracing";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TTag
+{
+ required string key = 1;
+ required string value = 2;
+}
+
+message TSpan
+{
+ required NYT.NProto.TGuid trace_id = 1;
+ required fixed64 span_id = 2;
+ optional fixed64 parent_span_id = 3;
+ optional fixed64 follows_from_span_id = 4;
+ required string name = 5;
+
+ required fixed64 start_time = 6;
+ required fixed64 duration = 7;
+
+ repeated TTag tags = 8;
+}
+
+message TSpanBatch
+{
+ repeated TSpan spans = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
diff --git a/yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto b/yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto
new file mode 100644
index 0000000000..8c2740493f
--- /dev/null
+++ b/yt/yt_proto/yt/core/tracing/proto/tracing_ext.proto
@@ -0,0 +1,22 @@
+package NYT.NTracing.NProto;
+
+import "yt_proto/yt/core/misc/proto/guid.proto";
+
+option java_package = "tech.ytsaurus.tracing";
+option java_multiple_files = true;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/tracing";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TTracingExt
+{
+ reserved 1, 2, 3;
+
+ optional NYT.NProto.TGuid trace_id = 4;
+ optional fixed64 span_id = 5;
+ optional bool sampled = 6;
+ optional bool debug = 7;
+ optional string target_endpoint = 9;
+ optional bytes baggage = 8;
+}
diff --git a/yt/yt_proto/yt/core/ya.make b/yt/yt_proto/yt/core/ya.make
new file mode 100644
index 0000000000..01de274fe4
--- /dev/null
+++ b/yt/yt_proto/yt/core/ya.make
@@ -0,0 +1,32 @@
+PROTO_LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/gradle.inc)
+
+SRCS(
+ crypto/proto/crypto.proto
+
+ misc/proto/bloom_filter.proto
+ misc/proto/error.proto
+ misc/proto/guid.proto
+ misc/proto/protobuf_helpers.proto
+
+ tracing/proto/span.proto
+ tracing/proto/tracing_ext.proto
+
+ bus/proto/bus.proto
+
+ rpc/proto/rpc.proto
+
+ yson/proto/protobuf_interop.proto
+
+ ytree/proto/attributes.proto
+ ytree/proto/ypath.proto
+)
+
+PROTO_NAMESPACE(yt)
+
+PY_NAMESPACE(yt_proto.yt.core)
+
+EXCLUDE_TAGS(GO_PROTO)
+
+END()
diff --git a/yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto b/yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto
new file mode 100644
index 0000000000..7aca26d4c9
--- /dev/null
+++ b/yt/yt_proto/yt/core/yson/proto/protobuf_interop.proto
@@ -0,0 +1,30 @@
+package NYT.NYson.NProto;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/yson";
+
+
+import "google/protobuf/descriptor.proto";
+
+extend google.protobuf.FieldOptions
+{
+ optional string field_name = 3000;
+ repeated string field_name_alias = 3003;
+ optional bool yson_string = 3001;
+ optional bool yson_map = 3002;
+ optional bool required = 3004;
+}
+
+extend google.protobuf.EnumValueOptions
+{
+ optional string enum_value_name = 3001;
+}
+
+extend google.protobuf.MessageOptions
+{
+ optional bool attribute_dictionary = 3000;
+}
+
+extend google.protobuf.FileOptions
+{
+ optional bool derive_underscore_case_names = 3000;
+}
diff --git a/yt/yt_proto/yt/core/ytree/proto/attributes.proto b/yt/yt_proto/yt/core/ytree/proto/attributes.proto
new file mode 100644
index 0000000000..cb1b90451e
--- /dev/null
+++ b/yt/yt_proto/yt/core/ytree/proto/attributes.proto
@@ -0,0 +1,30 @@
+package NYT.NYTree.NProto;
+
+import "yt_proto/yt/core/yson/proto/protobuf_interop.proto";
+
+option java_package = "tech.ytsaurus.ytree";
+option java_multiple_files = true;
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/ytree";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TAttribute
+{
+ required string key = 1;
+ required bytes value = 2;
+}
+
+message TAttributeDictionary
+{
+ option (NYT.NYson.NProto.attribute_dictionary) = true;
+ repeated TAttribute attributes = 1;
+}
+
+message TAttributeFilter
+{
+ repeated string keys = 1;
+ repeated string paths = 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt_proto/yt/core/ytree/proto/ypath.proto b/yt/yt_proto/yt/core/ytree/proto/ypath.proto
new file mode 100644
index 0000000000..5fb905d41e
--- /dev/null
+++ b/yt/yt_proto/yt/core/ytree/proto/ypath.proto
@@ -0,0 +1,158 @@
+package NYT.NYTree.NProto;
+
+import "yt_proto/yt/core/rpc/proto/rpc.proto";
+import "yt_proto/yt/core/ytree/proto/attributes.proto";
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/ytree";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TYPathHeaderExt
+{
+ extend NRpc.NProto.TRequestHeader
+ {
+ optional TYPathHeaderExt ypath_header_ext = 104;
+ }
+
+ //! True if the request changes the persistent state (e.g. Set, Remove, Create etc).
+ optional bool mutating = 1;
+
+ //! The primary object targeted by this request.
+ required string target_path = 2;
+
+ //! A copy of target_path in case of path rewrite.
+ optional string original_target_path = 3;
+
+ //! In case of multivalent methods (e.g. Copy) contains the paths of other
+ //! involved objects.
+ repeated string additional_paths = 4;
+
+ //! A copy of additional_paths in case of path rewrite.
+ repeated string original_additional_paths = 5;
+
+ reserved 6;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TCachingHeaderExt
+{
+ extend NRpc.NProto.TRequestHeader
+ {
+ optional TCachingHeaderExt caching_header_ext = 105;
+ }
+
+ required int32 expire_after_successful_update_time = 1;
+ required int32 expire_after_failed_update_time = 2;
+ optional int64 refresh_revision = 3;
+ optional bool disable_second_level_cache = 4;
+ optional bool disable_per_user_cache = 5;
+ optional int64 success_staleness_bound = 6;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqGet
+{
+ // TODO(max42): rename attributes -> attribute_filter
+ optional TAttributeFilter attributes = 1;
+ optional int64 limit = 2;
+ optional bool ignore_opaque = 3;
+ optional NYT.NYTree.NProto.TAttributeDictionary options = 4;
+}
+
+message TRspGet
+{
+ required bytes value = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqGetKey
+{ }
+
+message TRspGetKey
+{
+ required bytes value = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqSet
+{
+ required bytes value = 1;
+ optional bool recursive = 2 [default = false];
+ optional bool force = 3 [default = false];
+}
+
+message TRspSet
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqRemove
+{
+ optional bool recursive = 1 [default = true];
+ optional bool force = 2 [default = false];
+}
+
+message TRspRemove
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqList
+{
+ optional TAttributeFilter attributes = 1;
+ optional int64 limit = 2;
+}
+
+message TRspList
+{
+ required bytes value = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqExists
+{ }
+
+message TRspExists
+{
+ required bool value = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// COMPAT(gritukan) This is an alias for TReqMultisetAttributes.
+message TReqMultiset
+{
+ message TSubrequest
+ {
+ required string key = 1;
+ required bytes value = 2;
+ }
+
+ repeated TSubrequest subrequests = 1;
+}
+
+message TRspMultiset
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqMultisetAttributes
+{
+ message TSubrequest
+ {
+ required string attribute = 1;
+ required bytes value = 2;
+ }
+
+ repeated TSubrequest subrequests = 1;
+}
+
+message TRspMultisetAttributes
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt_proto/yt/formats/CMakeLists.darwin-x86_64.txt b/yt/yt_proto/yt/formats/CMakeLists.darwin-x86_64.txt
new file mode 100644
index 0000000000..031fc7a562
--- /dev/null
+++ b/yt/yt_proto/yt/formats/CMakeLists.darwin-x86_64.txt
@@ -0,0 +1,56 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-formats)
+target_link_libraries(yt_proto-yt-formats PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-formats PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/extension.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/yamr.proto
+)
+target_proto_addincls(yt_proto-yt-formats
+ ./
+ ${CMAKE_SOURCE_DIR}/
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-formats
+ --cpp_out=${CMAKE_BINARY_DIR}/
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/
+)
diff --git a/yt/yt_proto/yt/formats/CMakeLists.linux-aarch64.txt b/yt/yt_proto/yt/formats/CMakeLists.linux-aarch64.txt
new file mode 100644
index 0000000000..b562859693
--- /dev/null
+++ b/yt/yt_proto/yt/formats/CMakeLists.linux-aarch64.txt
@@ -0,0 +1,57 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-formats)
+target_link_libraries(yt_proto-yt-formats PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-formats PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/extension.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/yamr.proto
+)
+target_proto_addincls(yt_proto-yt-formats
+ ./
+ ${CMAKE_SOURCE_DIR}/
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-formats
+ --cpp_out=${CMAKE_BINARY_DIR}/
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/
+)
diff --git a/yt/yt_proto/yt/formats/CMakeLists.linux-x86_64.txt b/yt/yt_proto/yt/formats/CMakeLists.linux-x86_64.txt
new file mode 100644
index 0000000000..b562859693
--- /dev/null
+++ b/yt/yt_proto/yt/formats/CMakeLists.linux-x86_64.txt
@@ -0,0 +1,57 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-formats)
+target_link_libraries(yt_proto-yt-formats PUBLIC
+ contrib-libs-linux-headers
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-formats PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/extension.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/yamr.proto
+)
+target_proto_addincls(yt_proto-yt-formats
+ ./
+ ${CMAKE_SOURCE_DIR}/
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-formats
+ --cpp_out=${CMAKE_BINARY_DIR}/
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/
+)
diff --git a/yt/yt_proto/yt/formats/CMakeLists.txt b/yt/yt_proto/yt/formats/CMakeLists.txt
new file mode 100644
index 0000000000..f8b31df0c1
--- /dev/null
+++ b/yt/yt_proto/yt/formats/CMakeLists.txt
@@ -0,0 +1,17 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-aarch64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
+ include(CMakeLists.darwin-x86_64.txt)
+elseif (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" AND NOT HAVE_CUDA)
+ include(CMakeLists.windows-x86_64.txt)
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND NOT HAVE_CUDA)
+ include(CMakeLists.linux-x86_64.txt)
+endif()
diff --git a/yt/yt_proto/yt/formats/CMakeLists.windows-x86_64.txt b/yt/yt_proto/yt/formats/CMakeLists.windows-x86_64.txt
new file mode 100644
index 0000000000..031fc7a562
--- /dev/null
+++ b/yt/yt_proto/yt/formats/CMakeLists.windows-x86_64.txt
@@ -0,0 +1,56 @@
+
+# This file was generated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+get_built_tool_path(
+ TOOL_protoc_bin
+ TOOL_protoc_dependency
+ contrib/tools/protoc/bin
+ protoc
+)
+get_built_tool_path(
+ TOOL_cpp_styleguide_bin
+ TOOL_cpp_styleguide_dependency
+ contrib/tools/protoc/plugins/cpp_styleguide
+ cpp_styleguide
+)
+
+add_library(yt_proto-yt-formats)
+target_link_libraries(yt_proto-yt-formats PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ contrib-libs-protobuf
+)
+target_proto_messages(yt_proto-yt-formats PRIVATE
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/extension.proto
+ ${CMAKE_SOURCE_DIR}/yt/yt_proto/yt/formats/yamr.proto
+)
+target_proto_addincls(yt_proto-yt-formats
+ ./
+ ${CMAKE_SOURCE_DIR}/
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/contrib/libs/protobuf/src
+)
+target_proto_outs(yt_proto-yt-formats
+ --cpp_out=${CMAKE_BINARY_DIR}/
+ --cpp_styleguide_out=${CMAKE_BINARY_DIR}/
+)
diff --git a/yt/yt_proto/yt/formats/extension.proto b/yt/yt_proto/yt/formats/extension.proto
new file mode 100644
index 0000000000..80a529ba95
--- /dev/null
+++ b/yt/yt_proto/yt/formats/extension.proto
@@ -0,0 +1,107 @@
+import "google/protobuf/descriptor.proto";
+
+package NYT;
+
+option go_package = "a.yandex-team.ru/yt/yt_proto/yt/formats";
+
+message EWrapperFieldFlag
+{
+ enum Enum
+ {
+ ANY = 0;
+ OTHER_COLUMNS = 1;
+ ENUM_INT = 2;
+ ENUM_STRING = 3;
+
+ // Fields marked by this flag may correspond to composite types in table,
+ // e.g. message to Struct, repeated to List etc.
+ SERIALIZATION_YT = 4;
+
+ // Message fields marked by this flag correspond to String column in table
+ // (containing serialized representation of the field).
+ SERIALIZATION_PROTOBUF = 5;
+
+ // repeated field corresponds to List<T> type.
+ REQUIRED_LIST = 6;
+
+ // repeated field corresponds to Optional<List<T>> type.
+ OPTIONAL_LIST = 7;
+
+ // map<Key, Value> corresponds to List<Struct<key: Key, value: String>>, i.e.
+ // the type for Value is inferred as if it had SERIALIZATION_PROTOBUF flag.
+ MAP_AS_LIST_OF_STRUCTS_LEGACY = 8;
+
+ // map<Key, Value> corresponds to List<Struct<key: Key, value: Inferred(Value)>>,
+ // where Inferred(Value) is the type inferred as if Value had SERIALIZATION_YT flag,
+ // e.g. Struct for message type.
+ MAP_AS_LIST_OF_STRUCTS = 9;
+
+ // map<Key, Value> corresponds to Dict<Key, Inferred(Value)>.
+ // (the option is experimental, see YT-13314)
+ MAP_AS_DICT = 10;
+
+ // map<Key, Value> corresponds to Optional<Dict<Key, Inferred(Value)>>.
+ // (the option is experimental, see YT-13314)
+ MAP_AS_OPTIONAL_DICT = 11;
+
+ // ignore this field but unfold subfields instead while (de)serializing
+ EMBEDDED = 12;
+
+ // Optional enum fields marked by this flag would not be filled if they hold
+ // a constant, that is missing in protobuf schema. (good for forward compatibility)
+ ENUM_SKIP_UNKNOWN_VALUES = 13;
+
+ // Enum fields marked by this flag could only be filled with constants
+ // that are present in the protobuf schema. (default behaviour)
+ ENUM_CHECK_VALUES = 14;
+ }
+}
+
+message EWrapperMessageFlag
+{
+ enum Enum
+ {
+ DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE = 0;
+ SORT_FIELDS_BY_FIELD_NUMBER = 1;
+ }
+}
+
+message EWrapperOneofFlag
+{
+ enum Enum
+ {
+ // Each field inside oneof group corresponds to a regular field of the
+ // parent message.
+ SEPARATE_FIELDS = 12;
+
+ // Oneof group corresponds to Variant over Struct.
+ VARIANT = 13;
+ }
+}
+
+extend google.protobuf.FileOptions
+{
+ repeated EWrapperOneofFlag.Enum file_default_oneof_flags = 56788;
+ repeated EWrapperFieldFlag.Enum file_default_field_flags = 56789;
+ repeated EWrapperMessageFlag.Enum file_default_message_flags = 56790;
+}
+
+extend google.protobuf.FieldOptions
+{
+ repeated EWrapperFieldFlag.Enum flags = 56788;
+ optional string column_name = 56789;
+ optional string key_column_name = 56790;
+}
+
+extend google.protobuf.MessageOptions
+{
+ repeated EWrapperOneofFlag.Enum default_oneof_flags = 56788;
+ repeated EWrapperFieldFlag.Enum default_field_flags = 56789;
+ repeated EWrapperMessageFlag.Enum message_flags = 56790;
+}
+
+extend google.protobuf.OneofOptions
+{
+ repeated EWrapperOneofFlag.Enum oneof_flags = 56788;
+ optional string variant_field_name = 56789;
+}
diff --git a/yt/yt_proto/yt/formats/ya.make b/yt/yt_proto/yt/formats/ya.make
new file mode 100644
index 0000000000..b73e73ba1c
--- /dev/null
+++ b/yt/yt_proto/yt/formats/ya.make
@@ -0,0 +1,14 @@
+PROTO_LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/gradle.inc)
+
+IF (NOT PY_PROTOS_FOR)
+ INCLUDE_TAGS(GO_PROTO)
+ENDIF()
+
+SRCS(
+ extension.proto
+ yamr.proto
+)
+
+END()
diff --git a/yt/yt_proto/yt/formats/yamr.proto b/yt/yt_proto/yt/formats/yamr.proto
new file mode 100644
index 0000000000..457de1f9fc
--- /dev/null
+++ b/yt/yt_proto/yt/formats/yamr.proto
@@ -0,0 +1,16 @@
+import "yt/yt_proto/yt/formats/extension.proto";
+
+package NYT;
+
+option go_package = "a.yandex-team.ru/yt/yt_proto/yt/formats";
+
+message TYamr {
+ optional string Key = 1 [(NYT.key_column_name) = "key"];
+ optional string Subkey = 2 [(NYT.key_column_name) = "subkey"];
+ optional string Value = 3 [(NYT.column_name) = "value"];
+}
+
+message TYamrNoSubkey {
+ optional string Key = 1 [(NYT.key_column_name) = "key"];
+ optional string Value = 2 [(NYT.column_name) = "value"];
+}