From 0f7f208dccfeba1cc69081f4c27a018a72f92ecb Mon Sep 17 00:00:00 2001 From: Nick Vatamaniuc Date: Mon, 5 May 2025 13:10:31 -0400 Subject: [PATCH 1/6] Cherry-pick main changes into 3.5.x This is an amalgamated commit from these commits: edit d6e284e58 # Protect 3.5.x branch fixup 0b85b414a # dev/run: complete Erlang cookie configuration fixup 9cd775263 # No cfile support for 32bit systems fixup 8ae40d887 # Update QuickJS: FP16 support, regex escape compat fixup 942c94e3b # Fix mem3_util overlapping shards fixup c6fc3cb77 # Always allow mem3_rep checkpoints fixup 5693d7251 # Add retries to native full CI stage fixup 1fe50d703 # Ignore extraneous cookie in replicator session plugin fixup 03f0acc5a # Enable Clouseau for the Windows CI fixup 0da8cf2d4 # Bump Clouseau to 2.25.0 fixup ed763ef59 # Bump process limit to 1M fixup 234a01f15 # doc: add https to allowed replication proxy protocols fixup 859fbcdba # Improve `couch_debug.erl` (#5542) fixup fa2d5243d # Fix config key typo in mem3_reshard_dbdoc fixup fdb7cc156 # Fix reshard doc section name fixup 7397df54a # Don't spawn more than one init_delete_dir instance fixup 65f97ae54 # Improve init_delete_dir fixup 6e885301c # QuickJS update fixup 88f424cc9 # Improve `clouseau_rpc.erl` fixup 0c6237e59 # Handle shard opener tables not being initializes better fixup dc9837f04 # Improve mem3 supervisor fixup 2574fd18c # Improve cluster startup logging fixup adbc9489b # Fix QuickJS bigint heap overflow fixup 3cfd7e418 # Handle `bad_generator` and `case_clause` in ken_server fixup 7ef02ab16 # Improve replicator client mailbox flush fixup 20701cb68 # Add guards to `fabric:design_docs/1` to prevent function_clause error fixup b2cbfb875 # Bump requests to 2.32.4 and set trust_env=False fixup 004783446 # Set Erlang/OTP 26 as minimum supported version fixup 87007992e # Use the built-in binary hex encode fixup fff371bc1 # Use maps comprehensions and generators in a few places fixup cedeb738e # doc: add AI policy h/t @dch fixup 7a8e19ecb # Remove lto for quickjs fixup a179d0c82 # Skip macos CI for now and remove Ubuntu Focal fixup 02e2930ef # Upgrade Erlang for CI fixup 5ee3d737c # Remove a few more instances of Ubuntu Focal fixup 1a58a1cf7 # Update QuickJS fixup 6e0e8f39c # Run test262 JS conformance tests for QuickJS fixup bd0139e62 # Document how to mitigate high memory usage in docker fixup 1b62f06ea # wip suggestion for fsync pr fixup 5e658d324 # Add tests for write_header/3 with [sync] fixup ce1042338 # Use couch_file:write_header(., [sync]) in couch_bt_engine:commit_data/1 fixup 8f2c253fd # docs/replication: change unauthorized example to 401 fixup aae623bce # Don't wait indefinitely for replication jobs to stop fixup e3e445f28 # Improve mem3_rep:find_source_seq/4 logging fixup 58c56148e # fix couch_util:set_value/3 fixup 2cf6e558b # Populate zone from `COUCHDB_ZONE` env variable fixup febba3efa # update docs fixup 66ed28358 # Avoid making a mess in the logs when stopping replicator app fixup a5db3138e # Make replicator shutdown a bit more orderly fixup 786dc5d13 # Fix query args parsing during cluster upgrades fixup 9b2c1febd # Print the name of the failed property test fixup c609d4779 # nouveau: use http/2 fixup 4d24a701c # chore(docs): avoid master wording at setup cluster fixup 074e3a476 # don't start gun if nouveau disabled fixup 2fc406b8a # address PR feedback - move state to gen_server fixup b7adda961 # use ASF fork of gun for cowlib dep fixup 25acff4e7 # fix CI fixup 66996a1cd # docs: securing nouveau on http/2 fixup b936d99ff # Increase some test timeouts to hopefully fix a flaky PPC64LE test fixup e5175d1d6 # Add a range_to_hex/1 utility function fixup 540ca24d9 # Improve scanner performance fixup bdc7904b0 # Stop replication jobs to nodes which are not part of the cluster fixup b22adbb2e # In the scanner, traverse docs by sequence instead of by ID. fixup 27888d91e # DRY out couch_bt_engine header pointer term access fixup d25ff9175 # Implement the ability to downgrade CouchDB versions fixup f8c5938c7 # allow stale queries fixup a2ae7e332 # optimize searches when index is fresh fixup 2e49f7477 # stale test fixup 35f3d4f6d # Use copy_props in the compactor instead of set_props fixup bf8f48978 # Minor couch_btree refactoring fixup bb2083096 # retry if no connection available fixup 36b19b46d # switch to couch_util:to_hex_bin fixup 0c15f0f92 # gun takes iodata so no need for io_lib or flatten fixup 7a1584086 # QuickJS Update fixup 8c3df0385 # include rev when scanning fixup 866348c60 # Avoid timeouts in ddoc scanner callback fixup 044f74641 # enhance _nouveau_cleanup fixup 89b42aba0 # BTree engine term cache fixup 6568e2a4f # Use config:get_integer/3 in couch_btree fixup 6fb33bab3 # Fix and improve couch_btree testing fixup d4f421184 # Prevent B-tree duplicate entries. Add property tests. fixup 9db197a60 # Reduce btree prop test count a bit fixup 29518a402 # Use OS certificates for replication fixup 8f086d17e # Allow user to customize the `Timeout` threshold for checking search services fixup df2ff6342 # Remove redundant *_to_list / list_to_* conversion fixup 4a6ce811f # Fix reduce_limit = log feature fixup 5ccfa2e77 # Configure ELP fixup 00b6849f5 # Add write limiting to the scanner fixup 2802610b5 # Upgrade erlfmt and rebar3 fixup 0cc1ba499 # Implement prop updates for shards fixup 29f3992bb # Document that _all_dbs endpoint supports inclusive_end query param fixup 8db6286f4 # Retry call to dreyfus index on noproc errors fixup a0b43b120 # Remove absolete clauses from dreyfus fixup f51f6084f # Remove pointless message fixup 0e1591556 # Add delay before retry fixup e4979be7e # dev: support multiarch names (Debian based systems) (#5626) fixup 3bf07be54 # Use upgraded Erlang CI images fixup 3969b4019 # fix homebrew spidermonkey build fixup d17400591 # fix centos/freebsd build fixup 030bb8fee # Unify CI jobs fixup 2a96d0f01 # Remove old Jenkinsfiles fixup 420863397 # more informative error if epochs out of order fixup c3c98e249 # Disable ppc64le and s390x builds fixup 3444f5224 # Add Trixie to CI fixup e1c60c30d # Update mochiweb to v3.3.0 fixup b2a5a93dc # Update xxHash fixup 7a32ab7aa # Update QuickJS fixup 7bcf1a5a9 # fix make clean after dev/run --enable-tls fixup 3754deb3a # Cache and store mem3 shard properties in one place only fixup 2caf34166 # Print request/response body on errors from mango test suite fixup 8ab2dbc2e # Add setup documentation for two factor authentication (#5674) fixup fffa62e4a # Replace `gen_server:format_status/2` with `format_status/1` fixup c52d67d2c # Fix config options (#5642) fixup 01520a69e # Handle plugin stops without crashing fixup e65aa5ecc # Reschedule scanner plugins if they return skip on start or resume fixup 55b53273c # feat(configure): add --disable-spidermonkey to --dev[-with-nouveau] fixup 4f7f46f27 # Implement db doc updating fixup ba771b49b # Update QuickJS fixup 7066c8e43 # doc: update install instructions fixup 1908ea2c8 # Fix run_on_first_node scanner features fixup ee2bfe8f0 # Sequestrate docker ARM builds and fail early fixup 0266498eb # Handle timeout in `dreyfus_fabric_search:go` fixup abe1f6efa # Fix props caching in mem3 fixup 1eb2ea54d # QuickJS Update fixup 31de49a20 # Fix `case_clause` when got `missing_target` error fixup dac11b247 # Remove `erlang:` prefix from `erlang:error()` fixup d00248097 # Remove explicit erlang module prefix for auto-imported functions fixup d377697bc # Add assert comments to search related elixir tests fixup 0a1e26420 # Implement 'assert_on_status' macro fixup 639656802 # More QuickJS Optimization fixup 55fc7cba7 # Tighten the rules for parsing time periods fixup 2fc64fd28 # Add UUID v7 fixup 66a053c83 # Optimize purge fixup 7c33b2851 # Replace `dbg:stop_clear/0` with `dbg:stop/0` fixup 7ada53925 # Optimize revid parsing: 50-90% faster fixup cc27d73d0 # Use determistic doc IDs in Mango key test fixup 83aa0daf8 # Docs: Update the /_up endpoint docs to include status response's fixup 3b056091b # Remove purge max_document_id_number and change max_revisions_number fixup 3ecef046f # Update QuickJS: faster context creation and faster dbuf fixup e76a8b6ba # Increase timeout for `process_response/3` to fix flaky tests fixup 416e0776e # QuickJS Update. Optimized string operations. fixup 2903cd5f0 # improve search test fixup 2a9c1bd41 # Avoid using function closures in mem3 fixup 76577f3ab # Optimize purge. ~30% for large batches. fixup 0182fd1cf # Improve index cleanup fixup c543a88d7 # Cleanup fabric r/w parameter handling fixup 743312c75 # Remove hastings references fixup 74039c4c7 # Do not check for dreyfus fixup db57168cf # fixup: remove HAVE_DREYFUS refs fixup 458bc0de8 # Update elp toml file fixup e3583db45 # Use "all" ring options for purged_infos fixup 39cb8c539 # Update deps: fauxton, meck and proper fixup 516df2387 # Fix typo in .elp.toml fixup 53a6db7fd # Ignore design docs in the shards db in the scanner fixup da9afabb9 # Update Quickjs: optimize global var access, fix use-after-free error fixup df9a9a732 # Fix a few regressions recent updates fixup c71e71730 # parameterise reduce_limit threshold and ratio --- .asf.yaml | 3 + .elp.toml | 38 + .gitignore | 2 + CONTRIBUTING.md | 13 + INSTALL.Unix.md | 18 +- Makefile | 19 +- Makefile.win | 4 + README-DEV.rst | 12 +- build-aux/{Jenkinsfile.full => Jenkinsfile} | 387 +- build-aux/Jenkinsfile.pr | 293 - build-aux/xref-helper.sh | 1 - configure | 10 +- configure.ps1 | 8 +- dev/run | 23 +- .../org/apache/couchdb/nouveau/api/Ok.java | 26 + .../couchdb/nouveau/core/IndexManager.java | 61 +- .../nouveau/resources/IndexResource.java | 26 +- .../nouveau/core/IndexManagerTest.java | 77 +- rebar.config.script | 13 +- rel/nouveau.yaml | 4 +- rel/overlay/etc/default.ini | 110 +- rel/overlay/etc/vm.args | 11 + rel/reltool.config | 4 + share/server/views.js | 4 +- src/chttpd/src/chttpd_db.erl | 40 +- src/chttpd/src/chttpd_external.erl | 2 +- src/chttpd/src/chttpd_misc.erl | 4 +- src/chttpd/src/chttpd_node.erl | 3 +- .../chttpd_auth_hash_algorithms_tests.erl | 2 +- src/chttpd/test/eunit/chttpd_purge_tests.erl | 34 +- src/config/src/config.erl | 4 +- src/config/src/config_listener_mon.erl | 2 +- src/config/test/config_tests.erl | 38 +- src/couch/include/couch_db.hrl | 10 - src/couch/include/couch_eunit.hrl | 2 +- src/couch/include/couch_eunit_proper.hrl | 4 +- src/couch/priv/couch_cfile/couch_cfile.c | 30 +- src/couch/priv/stats_descriptions.cfg | 12 + src/couch/rebar.config.script | 64 +- src/couch/src/couch_att.erl | 12 +- src/couch/src/couch_base32.erl | 2 +- src/couch/src/couch_bt_engine.erl | 191 +- src/couch/src/couch_bt_engine_cache.erl | 292 + src/couch/src/couch_bt_engine_compactor.erl | 6 +- src/couch/src/couch_bt_engine_header.erl | 80 +- src/couch/src/couch_btree.erl | 362 +- src/couch/src/couch_db.erl | 78 +- src/couch/src/couch_db_updater.erl | 53 +- src/couch/src/couch_debug.erl | 55 +- src/couch/src/couch_doc.erl | 30 +- src/couch/src/couch_file.erl | 77 +- src/couch/src/couch_flags_config.erl | 2 +- src/couch/src/couch_httpd_auth.erl | 4 +- src/couch/src/couch_httpd_db.erl | 2 +- src/couch/src/couch_httpd_multipart.erl | 10 +- src/couch/src/couch_httpd_vhost.erl | 2 +- src/couch/src/couch_key_tree.erl | 6 +- src/couch/src/couch_multidb_changes.erl | 8 +- src/couch/src/couch_native_process.erl | 6 +- src/couch/src/couch_os_process.erl | 6 +- src/couch/src/couch_primary_sup.erl | 3 +- src/couch/src/couch_proc_manager.erl | 24 +- src/couch/src/couch_query_servers.erl | 13 +- src/couch/src/couch_server.erl | 11 +- src/couch/src/couch_stream.erl | 4 +- src/couch/src/couch_task_status.erl | 4 +- src/couch/src/couch_util.erl | 99 +- src/couch/src/couch_uuids.erl | 32 +- src/couch/src/test_util.erl | 6 +- .../test/eunit/couch_bt_engine_cache_test.erl | 102 + .../couch_bt_engine_compactor_ev_tests.erl | 6 +- .../eunit/couch_bt_engine_compactor_tests.erl | 4 +- .../test/eunit/couch_btree_prop_tests.erl | 225 + src/couch/test/eunit/couch_btree_tests.erl | 108 +- src/couch/test/eunit/couch_changes_tests.erl | 6 +- src/couch/test/eunit/couch_file_tests.erl | 83 + src/couch/test/eunit/couch_index_tests.erl | 2 +- .../test/eunit/couch_query_servers_tests.erl | 142 +- src/couch/test/eunit/couch_server_tests.erl | 8 +- src/couch/test/eunit/couch_stream_tests.erl | 2 +- .../test/eunit/couch_task_status_tests.erl | 4 +- .../test/eunit/couch_work_queue_tests.erl | 4 +- .../test/eunit/couchdb_attachments_tests.erl | 2 +- .../eunit/couchdb_file_compression_tests.erl | 2 +- src/couch/test/eunit/couchdb_os_proc_pool.erl | 10 +- .../eunit/couchdb_update_conflicts_tests.erl | 6 +- src/couch/test/eunit/couchdb_vhosts_tests.erl | 22 +- src/couch/test/eunit/couchdb_views_tests.erl | 12 +- src/couch_event/src/couch_event_listener.erl | 20 +- .../src/couch_event_listener_mfa.erl | 6 +- src/couch_event/src/couch_event_server.erl | 6 +- src/couch_index/src/couch_index.erl | 4 +- src/couch_index/src/couch_index_server.erl | 2 +- src/couch_index/src/couch_index_util.erl | 47 + .../eunit/couch_index_ddoc_updated_tests.erl | 4 +- src/couch_log/src/couch_log_config.erl | 2 +- src/couch_log/src/couch_log_trunc_io.erl | 10 +- src/couch_log/src/couch_log_trunc_io_fmt.erl | 14 +- .../eunit/couch_log_config_listener_test.erl | 4 +- .../test/eunit/couch_log_formatter_test.erl | 64 +- .../test/eunit/couch_log_server_test.erl | 4 +- .../test/eunit/couch_log_test_util.erl | 4 +- src/couch_mrview/src/couch_mrview.erl | 18 +- src/couch_mrview/src/couch_mrview_cleanup.erl | 51 +- .../src/couch_mrview_compactor.erl | 8 +- src/couch_mrview/src/couch_mrview_index.erl | 4 +- .../src/couch_mrview_test_util.erl | 2 +- src/couch_mrview/src/couch_mrview_updater.erl | 8 +- src/couch_mrview/src/couch_mrview_util.erl | 66 +- .../eunit/couch_mrview_collation_tests.erl | 6 +- .../test/eunit/couch_mrview_compact_tests.erl | 8 +- .../eunit/couch_mrview_ddoc_updated_tests.erl | 6 +- .../couch_mrview_purge_docs_fabric_tests.erl | 2 +- .../eunit/couch_mrview_purge_docs_tests.erl | 6 +- .../test/eunit/couch_mrview_util_tests.erl | 28 + src/couch_peruser/src/couch_peruser.erl | 2 +- src/couch_prometheus/src/couch_prometheus.erl | 20 +- .../test/eunit/couch_prometheus_e2e_tests.erl | 6 +- .../src/cpse_test_ref_counting.erl | 8 +- src/couch_pse_tests/src/cpse_util.erl | 18 +- src/couch_quickjs/.gitignore | 2 + src/couch_quickjs/c_src/couchjs.c | 27 +- .../patches/01-spidermonkey-185-mode.patch | 6 +- .../patches/02-test262-errors.patch | 11 + src/couch_quickjs/quickjs/Changelog | 28 + src/couch_quickjs/quickjs/Makefile | 13 + src/couch_quickjs/quickjs/VERSION | 2 +- src/couch_quickjs/quickjs/cutils.c | 52 +- src/couch_quickjs/quickjs/cutils.h | 102 +- src/couch_quickjs/quickjs/libregexp-opcode.h | 12 +- src/couch_quickjs/quickjs/libregexp.c | 1019 +- src/couch_quickjs/quickjs/libregexp.h | 1 + src/couch_quickjs/quickjs/libunicode-table.h | 421 +- src/couch_quickjs/quickjs/libunicode.c | 237 +- src/couch_quickjs/quickjs/libunicode.h | 14 +- src/couch_quickjs/quickjs/qjsc.c | 107 +- src/couch_quickjs/quickjs/quickjs-atom.h | 14 + src/couch_quickjs/quickjs/quickjs-libc.c | 260 +- src/couch_quickjs/quickjs/quickjs-libc.h | 8 +- src/couch_quickjs/quickjs/quickjs-opcode.h | 15 +- src/couch_quickjs/quickjs/quickjs.c | 8586 ++++++++++++----- src/couch_quickjs/quickjs/quickjs.h | 81 +- src/couch_quickjs/quickjs/run-test262.c | 114 +- src/couch_quickjs/quickjs/test262.conf | 93 +- src/couch_quickjs/quickjs/test262_errors.txt | 131 +- src/couch_quickjs/quickjs/tests/test262.patch | 41 +- src/couch_quickjs/rebar.config.script | 22 +- .../src/couch_quickjs_scanner_plugin.erl | 9 +- ...{update_and_apply_patches.sh => update.sh} | 8 +- src/couch_quickjs/update_patches.sh | 28 + .../src/couch_replicator_api_wrap.erl | 18 +- .../src/couch_replicator_auth_session.erl | 38 +- .../src/couch_replicator_connection.erl | 3 + .../couch_replicator_doc_processor_worker.erl | 2 +- .../src/couch_replicator_docs.erl | 2 +- .../src/couch_replicator_fabric.erl | 2 +- .../src/couch_replicator_fabric_rpc.erl | 10 +- .../src/couch_replicator_httpc.erl | 165 +- .../src/couch_replicator_httpc_pool.erl | 51 +- .../src/couch_replicator_notifier.erl | 30 +- .../src/couch_replicator_parse.erl | 25 +- .../src/couch_replicator_pg.erl | 14 +- .../src/couch_replicator_rate_limiter.erl | 6 +- .../src/couch_replicator_scheduler.erl | 913 +- .../src/couch_replicator_scheduler_job.erl | 130 +- .../src/couch_replicator_utils.erl | 54 +- .../src/couch_replicator_worker.erl | 82 +- .../src/json_stream_parse.erl | 2 +- .../eunit/couch_replicator_compact_tests.erl | 26 +- .../couch_replicator_connection_tests.erl | 2 +- ...couch_replicator_error_reporting_tests.erl | 2 +- .../couch_replicator_httpc_pool_tests.erl | 2 +- .../couch_replicator_rate_limiter_tests.erl | 2 +- ...plicator_retain_stats_between_job_runs.erl | 6 +- ...plicator_small_max_request_size_target.erl | 2 +- .../eunit/couch_replicator_test_helper.erl | 2 +- .../src/couch_scanner_plugin.erl | 193 +- .../couch_scanner_plugin_conflict_finder.erl | 10 +- .../src/couch_scanner_plugin_find.erl | 10 +- .../src/couch_scanner_rate_limiter.erl | 56 +- .../src/couch_scanner_server.erl | 2 + src/couch_scanner/src/couch_scanner_util.erl | 35 +- .../test/eunit/couch_scanner_test.erl | 54 +- src/couch_stats/src/couch_stats_httpd.erl | 4 +- .../src/couch_stats_process_tracker.erl | 2 +- src/custodian/src/custodian_util.erl | 2 +- src/ddoc_cache/src/ddoc_cache_entry.erl | 16 +- .../src/ddoc_cache_entry_validation_funs.erl | 13 +- src/ddoc_cache/src/ddoc_cache_lru.erl | 2 +- .../test/eunit/ddoc_cache_coverage_test.erl | 4 +- .../test/eunit/ddoc_cache_entry_test.erl | 4 +- .../test/eunit/ddoc_cache_lru_test.erl | 12 +- .../test/eunit/ddoc_cache_open_error_test.erl | 2 +- .../test/eunit/ddoc_cache_open_test.erl | 2 +- .../test/eunit/ddoc_cache_remove_test.erl | 4 +- .../test/eunit/ddoc_cache_tutil.erl | 2 +- src/docs/src/api/database/changes.rst | 5 +- src/docs/src/api/server/authn.rst | 91 + src/docs/src/api/server/common.rst | 20 +- src/docs/src/cluster/databases.rst | 3 + src/docs/src/cluster/purging.rst | 4 +- src/docs/src/cluster/sharding.rst | 5 +- src/docs/src/cluster/troubleshooting.rst | 2 +- src/docs/src/config/misc.rst | 33 +- src/docs/src/config/query-servers.rst | 14 +- src/docs/src/config/replicator.rst | 16 +- src/docs/src/config/resharding.rst | 2 +- src/docs/src/config/scanner.rst | 19 + src/docs/src/ddocs/ddocs.rst | 3 + src/docs/src/install/nouveau.rst | 11 +- src/docs/src/install/troubleshooting.rst | 10 + src/docs/src/install/unix.rst | 3 +- src/docs/src/replication/protocol.rst | 2 +- src/docs/src/setup/cluster.rst | 2 +- src/dreyfus/src/clouseau_rpc.erl | 90 +- src/dreyfus/src/dreyfus_fabric_cleanup.erl | 122 +- src/dreyfus/src/dreyfus_fabric_group1.erl | 4 +- src/dreyfus/src/dreyfus_fabric_group2.erl | 4 +- src/dreyfus/src/dreyfus_fabric_search.erl | 4 +- src/dreyfus/src/dreyfus_index.erl | 11 +- src/dreyfus/src/dreyfus_rpc.erl | 47 +- src/dreyfus/src/dreyfus_util.erl | 34 +- src/dreyfus/test/eunit/dreyfus_purge_test.erl | 8 +- src/ets_lru/src/ets_lru.erl | 2 +- src/ets_lru/test/ets_lru_test.erl | 4 +- src/exxhash/README.md | 2 +- src/exxhash/c_src/xxhash.h | 207 +- src/fabric/src/fabric.erl | 79 +- src/fabric/src/fabric_db_create.erl | 8 +- src/fabric/src/fabric_db_delete.erl | 2 +- src/fabric/src/fabric_db_meta.erl | 25 +- src/fabric/src/fabric_db_purged_infos.erl | 17 +- src/fabric/src/fabric_db_update_listener.erl | 10 +- src/fabric/src/fabric_doc_open.erl | 20 +- src/fabric/src/fabric_doc_open_revs.erl | 5 +- src/fabric/src/fabric_doc_purge.erl | 523 +- src/fabric/src/fabric_doc_update.erl | 3 +- src/fabric/src/fabric_index_cleanup.erl | 81 + src/fabric/src/fabric_open_revs.erl | 3 +- src/fabric/src/fabric_rpc.erl | 53 +- src/fabric/src/fabric_streams.erl | 14 +- src/fabric/src/fabric_util.erl | 184 +- src/fabric/src/fabric_view_all_docs.erl | 6 +- src/fabric/src/fabric_view_changes.erl | 4 +- src/fabric/src/fabric_view_map.erl | 2 +- src/fabric/test/eunit/fabric_bench_test.erl | 2 +- .../test/eunit/fabric_db_info_tests.erl | 38 +- .../eunit/fabric_moved_shards_seq_tests.erl | 2 +- .../test/eunit/fabric_rpc_purge_tests.erl | 8 +- src/fabric/test/eunit/fabric_rpc_tests.erl | 2 +- src/fabric/test/eunit/fabric_tests.erl | 23 +- .../src/global_changes_server.erl | 4 +- src/ioq/src/ioq.erl | 4 +- src/ken/rebar.config.script | 14 +- src/ken/src/ken.app.src.script | 13 +- src/ken/src/ken_server.erl | 92 +- src/ken/test/ken_server_test.erl | 2 +- src/mango/rebar.config.script | 16 +- src/mango/requirements.txt | 2 +- src/mango/src/mango_cursor.erl | 8 - src/mango/src/mango_cursor_nouveau.erl | 4 +- src/mango/src/mango_cursor_text.erl | 8 +- src/mango/src/mango_doc.erl | 10 +- src/mango/src/mango_idx_special.erl | 8 +- src/mango/src/mango_json.erl | 2 +- src/mango/src/mango_native_proc.erl | 2 +- src/mango/src/mango_selector.erl | 2 +- src/mango/src/mango_selector_text.erl | 8 +- src/mango/src/mango_util.erl | 16 +- src/mango/test/02-basic-find-test.py | 2 +- src/mango/test/04-key-tests.py | 7 +- src/mango/test/mango.py | 58 +- src/mem3/include/mem3.hrl | 4 +- src/mem3/src/mem3.erl | 74 +- src/mem3/src/mem3_db_doc_updater.erl | 107 + src/mem3/src/mem3_hash.erl | 30 +- src/mem3/src/mem3_httpd.erl | 4 +- src/mem3/src/mem3_nodes.erl | 61 +- src/mem3/src/mem3_rep.erl | 23 +- src/mem3/src/mem3_reshard.erl | 4 +- src/mem3/src/mem3_reshard_dbdoc.erl | 141 +- src/mem3/src/mem3_reshard_index.erl | 26 +- src/mem3/src/mem3_reshard_job.erl | 10 +- src/mem3/src/mem3_reshard_sup.erl | 3 - src/mem3/src/mem3_rpc.erl | 10 +- src/mem3/src/mem3_shards.erl | 387 +- src/mem3/src/mem3_sup.erl | 31 +- src/mem3/src/mem3_sync.erl | 43 +- src/mem3/src/mem3_sync_event_listener.erl | 4 +- src/mem3/src/mem3_sync_security.erl | 2 +- src/mem3/src/mem3_util.erl | 99 +- .../test/eunit/mem3_distribution_test.erl | 5 +- src/mem3/test/eunit/mem3_rep_test.erl | 10 +- .../eunit/mem3_reshard_changes_feed_test.erl | 10 +- src/mem3/test/eunit/mem3_reshard_test.erl | 79 +- src/mem3/test/eunit/mem3_shards_test.erl | 77 +- src/mem3/test/eunit/mem3_zone_test.erl | 77 + src/nouveau/src/nouveau.app.src | 2 +- src/nouveau/src/nouveau_api.erl | 298 +- src/nouveau/src/nouveau_bookmark.erl | 2 +- src/nouveau/src/nouveau_fabric_cleanup.erl | 70 +- src/nouveau/src/nouveau_gun.erl | 162 + src/nouveau/src/nouveau_index_manager.erl | 33 - src/nouveau/src/nouveau_index_updater.erl | 46 +- src/nouveau/src/nouveau_rpc.erl | 76 +- src/nouveau/src/nouveau_sup.erl | 1 + src/nouveau/src/nouveau_util.erl | 30 +- src/rexi/src/rexi.erl | 2 +- src/rexi/src/rexi_monitor.erl | 2 +- src/rexi/src/rexi_server.erl | 2 +- src/rexi/src/rexi_server_mon.erl | 9 +- src/rexi/src/rexi_utils.erl | 2 +- src/rexi/test/rexi_tests.erl | 4 +- src/setup/src/setup.erl | 2 +- src/setup/test/t-frontend-setup.sh | 4 +- src/smoosh/src/smoosh.erl | 9 +- src/smoosh/src/smoosh_channel.erl | 14 +- src/smoosh/src/smoosh_persist.erl | 4 +- src/smoosh/test/smoosh_tests.erl | 16 +- .../src/weatherreport_check_mem3_sync.erl | 2 +- .../src/weatherreport_check_node_stats.erl | 2 +- .../weatherreport_check_nodes_connected.erl | 2 +- .../src/weatherreport_check_process_calls.erl | 2 +- .../src/weatherreport_getopt.erl | 2 +- src/weatherreport/src/weatherreport_node.erl | 2 +- src/weatherreport/src/weatherreport_util.erl | 2 +- test/elixir/lib/asserts.ex | 20 + test/elixir/test/changes_async_test.exs | 2 +- test/elixir/test/config/nouveau.elixir | 3 +- test/elixir/test/nouveau_test.exs | 17 + test/elixir/test/partition_search_test.exs | 47 +- test/elixir/test/search_test.exs | 83 +- 332 files changed, 15261 insertions(+), 6806 deletions(-) create mode 100644 .elp.toml rename build-aux/{Jenkinsfile.full => Jenkinsfile} (65%) delete mode 100644 build-aux/Jenkinsfile.pr create mode 100644 nouveau/src/main/java/org/apache/couchdb/nouveau/api/Ok.java create mode 100644 src/couch/src/couch_bt_engine_cache.erl create mode 100644 src/couch/test/eunit/couch_bt_engine_cache_test.erl create mode 100644 src/couch/test/eunit/couch_btree_prop_tests.erl create mode 100644 src/couch_quickjs/patches/02-test262-errors.patch rename src/couch_quickjs/{update_and_apply_patches.sh => update.sh} (88%) create mode 100755 src/couch_quickjs/update_patches.sh create mode 100644 src/fabric/src/fabric_index_cleanup.erl create mode 100644 src/mem3/src/mem3_db_doc_updater.erl create mode 100644 src/mem3/test/eunit/mem3_zone_test.erl create mode 100644 src/nouveau/src/nouveau_gun.erl create mode 100644 test/elixir/lib/asserts.ex diff --git a/.asf.yaml b/.asf.yaml index 92afea9e4f..3e9abc121d 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -40,6 +40,9 @@ github: strict: true contexts: - continuous-integration/jenkins/pr-merge + 3.5.x: + required_status_checks: + strict: true 3.4.x: required_status_checks: strict: true diff --git a/.elp.toml b/.elp.toml new file mode 100644 index 0000000000..e28ee26aed --- /dev/null +++ b/.elp.toml @@ -0,0 +1,38 @@ +[build_info] +apps = ["src/*"] +# 3rd party dependencies (not type-checked), defaults to [] (see rebar.config.script) +deps = [ + "src/erlfmt", + "src/rebar", + "src/rebar3", + "src/meck", + "src/cowlib", + "src/gun", + "src/recon", + "src/proper", + "src/fauxton", + "src/docs", + "src/meck", + "src/jiffy", + "src/ibrowse", + "src/mochiweb", + "src/snappy" +] +# List of OTP application names to exclude from indexing. This can help improve performance by not loading rarely used OTP apps. +[otp] +exclude_apps = [ + "megaco", + "common_test", + "edoc", + "eldap", + "erl_docgen", + "et", + "ftp", + "mnesia", + "odbc", + "observer", + "snmp", + "tftp", + "wx", + "xmerl" +] diff --git a/.gitignore b/.gitignore index 080a7dd6ff..d46777512b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,12 @@ src/couch/priv/couchspawnkillable src/couch/priv/couch_ejson_compare/couch_ejson_compare.d src/couch/priv/couch_js/**/*.d src/couch/priv/icu_driver/couch_icu_driver.d +src/cowlib/ src/mango/src/mango_cursor_text.nocompile src/excoveralls/ src/fauxton/ src/folsom/ +src/gun/ src/hackney/ src/hqueue/ src/ibrowse/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ba7276ba8..f229fc1069 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -302,6 +302,19 @@ hub checkout link-to-pull-request meaning that you will automatically check out the branch for the pull request, without needing any other steps like setting git upstreams! :sparkles: +## Artificial Intelligence and Large Language Models Contributions Policy + +The CouchDB project has a long-standing focus on license compatibility, and +appropriate attribution of source code. AI and LLMs, by their nature, are unable +to provide the necessary assurance, that the generated material is compatible +with the Apache 2 license, or that the material has been appropriately +attributed to the original authors. + +Thus, it is expressly forbidden to contribute material generated by AI, LLMs, +and similar technologies, to the CouchDB project. This includes, but is not +limited to, source code, documentation, commit messages, or any other areas of +the project. + ## Thanks Special thanks to [Hoodie][#gh_hoodie] for the great diff --git a/INSTALL.Unix.md b/INSTALL.Unix.md index a078897987..a27ff626a0 100644 --- a/INSTALL.Unix.md +++ b/INSTALL.Unix.md @@ -1,6 +1,6 @@ # Apache CouchDB INSTALL.Unix -A high-level guide to Unix-like systems, inc. Mac OS X and Ubuntu. +A high-level guide to Unix-like systems, inc. macOS and Ubuntu. Community installation guides are available on the wiki: @@ -84,9 +84,9 @@ You can install the dependencies by running: You can install Node.JS via [NodeSource](https://github.com/nodesource/distributions#rpminstall). -### Mac OS X +### macOS -To build CouchDB from source on Mac OS X, you will need to install +To build CouchDB from source on macOS, you will need to install the Command Line Tools: xcode-select --install @@ -96,6 +96,12 @@ You can then install the other dependencies by running: brew install autoconf autoconf-archive automake libtool \ erlang icu4c spidermonkey pkg-config +Note that newer versions of Homebrew install `icu4c` as “keg-only”. +That means the CouchDB build system can’t find it. Either follow +the instructions presented by Homebrew or run + + brew link icu4c + You can install Node.JS via the [official Macintosh installer](https://nodejs.org/en/download/). @@ -103,9 +109,9 @@ You will need Homebrew installed to use the `brew` command. Learn more about Homebrew at: - http://mxcl.github.com/homebrew/ + https://brew.sh -Some versions of Mac OS X ship a problematic OpenSSL library. If +Some versions of macOS ship a problematic OpenSSL library. If you're experiencing troubles with CouchDB crashing intermittently with a segmentation fault or a bus error, you will need to install your own version of OpenSSL. See the wiki, mentioned above, for more information. @@ -169,7 +175,7 @@ On many Unix-like systems you can run: --group --gecos \ "CouchDB Administrator" couchdb -On Mac OS X you can use the Workgroup Manager to create users up to version +On macOS you can use the Workgroup Manager to create users up to version 10.9, and dscl or sysadminctl after version 10.9. Search Apple's support site to find the documentation appropriate for your system. As of recent versions of OS X, this functionality is also included in Server.app, diff --git a/Makefile b/Makefile index 8b1df51e65..6847037df7 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,10 @@ EXUNIT_OPTS=$(subst $(comma),$(space),$(tests)) TEST_OPTS="-c 'startup_jitter=0' -c 'default_security=admin_local' -c 'iterations=9'" +ifneq ($(ERLANG_COOKIE),) +TEST_OPTS+=" --erlang-cookie=$(ERLANG_COOKIE)" +endif + ################################################################################ # Main commands ################################################################################ @@ -350,6 +354,12 @@ weatherreport-test: devclean escriptize @dev/run "$(TEST_OPTS)" -n 1 -a adm:pass --no-eval \ 'bin/weatherreport --etc dev/lib/node1/etc --level error' +.PHONY: quickjs-test262 +# target: quickjs-javascript-tests - Run QuickJS JS conformance tests +quickjs-test262: couch + make -C src/couch_quickjs/quickjs test2-bootstrap + make -C src/couch_quickjs/quickjs test2 + ################################################################################ # Developing ################################################################################ @@ -474,7 +484,12 @@ clean: @rm -rf .rebar/ @rm -f bin/couchjs @rm -f bin/weatherreport - @rm -rf src/*/ebin + @find src/*/ebin \ + -not -path 'src/cowlib/ebin/cowlib.app' \ + -not -path 'src/cowlib/ebin' \ + -not -path 'src/gun/ebin/gun.app' \ + -not -path 'src/gun/ebin' \ + -delete @rm -rf src/*/.rebar @rm -rf src/*/priv/*.so @rm -rf share/server/main.js share/server/main-ast-bypass.js share/server/main-coffee.js @@ -482,7 +497,7 @@ clean: @rm -rf src/mango/.venv @rm -f src/couch/priv/couch_js/config.h @rm -f dev/*.beam dev/devnode.* dev/pbkdf2.pyc log/crash.log - @rm -f src/couch_dist/certs/out + @rm -rf src/couch_dist/certs/out @rm -rf src/docs/build src/docs/.venv ifeq ($(with_nouveau), true) @cd nouveau && $(GRADLE) clean diff --git a/Makefile.win b/Makefile.win index 000874b70b..d510ee51df 100644 --- a/Makefile.win +++ b/Makefile.win @@ -93,6 +93,10 @@ EXUNIT_OPTS=$(subst $(comma),$(space),$(tests)) TEST_OPTS="-c startup_jitter=0 -c default_security=admin_local -c iterations=9" +ifneq ($(ERLANG_COOKIE),) +TEST_OPTS+=" --erlang-cookie=$(ERLANG_COOKIE)" +endif + ################################################################################ # Main commands ################################################################################ diff --git a/README-DEV.rst b/README-DEV.rst index 1f0711b4a1..d6dcc4a558 100644 --- a/README-DEV.rst +++ b/README-DEV.rst @@ -111,8 +111,8 @@ Centos 7 and RHEL 7 python-pygments gnupg nodejs npm -Mac OS X -~~~~~~~~ +macOS +~~~~~ Install `Homebrew `_, if you do not have it already. @@ -120,7 +120,7 @@ Unless you want to install the optional dependencies, skip to the next section. Install what else we can with Homebrew:: - brew install help2man gnupg md5sha1sum node python + brew install help2man gnupg md5sha1sum node python elixir If you don't already have pip installed, install it:: @@ -311,9 +311,9 @@ follows:: dev/run --with-clouseau -When a specific Erlang cookie string is set in -``rel/overlay/etc/vm.args``, the ``--erlang-cookie`` flag could be -used to configure Clouseau to work with that:: +When a specific Erlang cookie string is needed, the +``--erlang-cookie`` flag could be used to configure CouchDB and +Clouseau to work with that:: dev/run --with-clouseau --erlang-cookie=brumbrum diff --git a/build-aux/Jenkinsfile.full b/build-aux/Jenkinsfile similarity index 65% rename from build-aux/Jenkinsfile.full rename to build-aux/Jenkinsfile index 479aec7ea2..dfba236c7b 100644 --- a/build-aux/Jenkinsfile.full +++ b/build-aux/Jenkinsfile @@ -13,12 +13,24 @@ // License for the specific language governing permissions and limitations under // the License. -// Erlang version embedded in binary packages -ERLANG_VERSION = '26.2.5.11' +// This is image used to build the tarball, check source formatting and build +// the docs +DOCKER_IMAGE_BASE = 'apache/couchdbci-debian:bookworm-erlang' + +// Erlang version embedded in binary packages. Also the version most builds +// will run. +ERLANG_VERSION = '26.2.5.15' // Erlang version used for rebar in release process. CouchDB will not build from // the release tarball on Erlang versions older than this -MINIMUM_ERLANG_VERSION = '25.3.2.20' +MINIMUM_ERLANG_VERSION = '26.2.5.15' + +// Highest support Erlang version. +MAXIMUM_ERLANG_VERSION = '28.0.4' + +// Use these to detect if just documents changed +docs_changed = "git diff --name-only origin/${env.CHANGE_TARGET} | grep -q '^src/docs/'" +other_changes = "git diff --name-only origin/${env.CHANGE_TARGET} | grep -q -v '^src/docs/'" // We create parallel build / test / package stages for each OS using the metadata // in this map. Adding a new OS should ideally only involve adding a new entry here. @@ -28,6 +40,7 @@ meta = [ spidermonkey_vsn: '60', with_nouveau: true, with_clouseau: true, + quickjs_test262: true, image: "apache/couchdbci-centos:8-erlang-${ERLANG_VERSION}" ], @@ -36,22 +49,16 @@ meta = [ spidermonkey_vsn: '78', with_nouveau: true, with_clouseau: true, + quickjs_test262: true, image: "apache/couchdbci-centos:9-erlang-${ERLANG_VERSION}" ], - 'focal': [ - name: 'Ubuntu 20.04', - spidermonkey_vsn: '68', - with_nouveau: true, - with_clouseau: true, - image: "apache/couchdbci-ubuntu:focal-erlang-${ERLANG_VERSION}" - ], - 'jammy': [ name: 'Ubuntu 22.04', spidermonkey_vsn: '91', with_nouveau: true, with_clouseau: true, + quickjs_test262: true, image: "apache/couchdbci-ubuntu:jammy-erlang-${ERLANG_VERSION}" ], @@ -60,48 +67,106 @@ meta = [ spidermonkey_vsn: '115', with_nouveau: true, with_clouseau: true, + quickjs_test262: true, image: "apache/couchdbci-ubuntu:noble-erlang-${ERLANG_VERSION}" ], - 'bookworm-ppc64': [ - name: 'Debian POWER', + 'bullseye': [ + name: 'Debian x86_64', spidermonkey_vsn: '78', with_nouveau: true, with_clouseau: true, - image: "apache/couchdbci-debian:bookworm-erlang-${ERLANG_VERSION}", - node_label: 'ppc64le' + quickjs_test262: true, + image: "apache/couchdbci-debian:bullseye-erlang-${ERLANG_VERSION}" ], - 'bookworm-s390x': [ - name: 'Debian s390x', + // Sometimes we "pick up" ppc64le workers from the asf jenkins intance That + // seems like a good thing, however, those workers allow running multiple + // agents at a time and often time out even with retries. The build times + // take close to an hour, which is at least x2 as long as it takes to run + // other arch CI jobs and they still time out often and fail. Disable for + // now. This is a low demand arch distro, maybe remove support altogether? + // + // 'base-ppc64': [ + // name: 'Debian POWER', + // spidermonkey_vsn: '78', + // with_nouveau: true, + // with_clouseau: true, + // quickjs_test262: true, + // image: "${DOCKER_IMAGE_BASE}-${ERLANG_VERSION}", + // node_label: 'ppc64le' + // ], + + // Just like in the ppc64le case we sometimes "pick up" built-in s390x workers added to + // our jenkins, but since those are managed by us, our .mix/.venv/.hex packaging + // cache hack in /home/jenkins doesn't work, and so elixir tests fail. Skip this arch build + // until we figure out caching (probably need to use a proper caching plugin). + // + // 'base-s390x': [ + // name: 'Debian s390x', + // spidermonkey_vsn: '78', + // with_nouveau: true, + // // QuickJS test262 shows a discrepancy typedarray-arg-set-values-same-buffer-other-type.js + // // Test262Error: 51539607552,42,0,4,5,6,7,8 + // quickjs_test262: false, + // image: "${DOCKER_IMAGE_BASE}-${ERLANG_VERSION}", + // node_label: 's390x' + // ], + + 'base': [ + name: 'Debian x86_64', spidermonkey_vsn: '78', with_nouveau: true, - image: "apache/couchdbci-debian:bookworm-erlang-${ERLANG_VERSION}", - node_label: 's390x' + with_clouseau: true, + // Test this in in the bookworm-quickjs variant + quickjs_test262: false, + image: "${DOCKER_IMAGE_BASE}-${ERLANG_VERSION}" ], - 'bullseye': [ + 'base-max-erlang': [ name: 'Debian x86_64', spidermonkey_vsn: '78', with_nouveau: true, with_clouseau: true, - image: "apache/couchdbci-debian:bullseye-erlang-${ERLANG_VERSION}" + quickjs_test262: false, + image: "${DOCKER_IMAGE_BASE}-${MAXIMUM_ERLANG_VERSION}" ], - 'bookworm': [ - name: 'Debian x86_64', + 'base-quickjs': [ + name: 'Debian 12 with QuickJS', + disable_spidermonkey: true, + with_nouveau: true, + with_clouseau: true, + quickjs_test262: true, + image: "${DOCKER_IMAGE_BASE}-${ERLANG_VERSION}" + ], + + // This runs on a docker ARM64 host. Normally we should be able to run all + // ubuntu and centos containers on top any docker host, however spidermonkey + // 60 from CentOS cannot build on ARM64 so we're forced to separate it as a + // separate build usable for ubuntu/debian only and isolated it from other + // builds. At some point when we remove CentOS 8 or switch to QuickJS only, + // remove the docker-arm64 label on ubuntu-nc-arm64-12 node in Jenkins and + // remove this flavor + // + 'base-arm64': [ + name: 'Debian ARM64', spidermonkey_vsn: '78', with_nouveau: true, with_clouseau: true, - image: "apache/couchdbci-debian:bookworm-erlang-${ERLANG_VERSION}" + // Test this in in the bookworm-quickjs variant + quickjs_test262: false, + image: "${DOCKER_IMAGE_BASE}-${ERLANG_VERSION}", + node_label: 'docker-arm64' ], - 'bookworm-quickjs': [ - name: 'Debian 12 with QuickJS', - disable_spidermonkey: true, + 'trixie': [ + name: 'Debian x86_64', + spidermonkey_vsn: '128', with_nouveau: true, with_clouseau: true, - image: "apache/couchdbci-debian:bookworm-erlang-${ERLANG_VERSION}" + quickjs_test262: true, + image: "apache/couchdbci-debian:trixie-erlang-${ERLANG_VERSION}" ], 'freebsd-x86_64': [ @@ -109,6 +174,7 @@ meta = [ spidermonkey_vsn: '91', with_clouseau: true, clouseau_java_home: '/usr/local/openjdk8-jre', + quickjs_test262: false, gnu_make: 'gmake' ], @@ -119,21 +185,27 @@ meta = [ disable_spidermonkey: true, with_clouseau: true, clouseau_java_home: '/usr/local/openjdk8-jre', + quickjs_test262: false, gnu_make: 'gmake' ], - 'macos': [ - name: 'macOS', - spidermonkey_vsn: '128', - with_nouveau: false, - with_clouseau: true, - clouseau_java_home: '/opt/java/openjdk8/zulu-8.jre/Contents/Home', - gnu_make: 'make' - ], + // Disable temporarily. Forks / shell execs seem to fail there currently + // + // + // 'macos': [ + // name: 'macOS', + // spidermonkey_vsn: '128', + // with_nouveau: false, + // with_clouseau: true, + // clouseau_java_home: '/opt/java/openjdk8/zulu-8.jre/Contents/Home', + // gnu_make: 'make' + // ], 'win2022': [ name: 'Windows 2022', spidermonkey_vsn: '128', + with_clouseau: true, + quickjs_test262: false, node_label: 'win' ] ] @@ -170,7 +242,7 @@ def generateNativeStage(platform) { return { stage(platform) { node(platform) { - timeout(time: 90, unit: "MINUTES") { + timeout(time: 180, unit: "MINUTES") { // Steps to configure and build CouchDB on *nix platforms if (isUnix()) { try { @@ -190,12 +262,13 @@ def generateNativeStage(platform) { dir( "${platform}/build" ) { sh "${configure(meta[platform])}" sh '$MAKE' - sh '$MAKE eunit' - sh '$MAKE elixir' - sh '$MAKE elixir-search' - sh '$MAKE mango-test' - sh '$MAKE weatherreport-test' - sh '$MAKE nouveau-test' + retry (3) {sh '$MAKE eunit'} + if (meta[platform].quickjs_test262) {retry(3) {sh 'make quickjs-test262'}} + retry (3) {sh '$MAKE elixir'} + retry (3) {sh '$MAKE elixir-search'} + retry (3) {sh '$MAKE mango-test'} + retry (3) {sh '$MAKE weatherreport-test'} + retry (3) {sh '$MAKE nouveau-test'} } } } @@ -229,9 +302,11 @@ def generateNativeStage(platform) { powershell( script: "New-Item -ItemType Directory -Path '${platform}/build' -Force", label: 'Create build directories' ) powershell( script: "tar -xf (Get-Item apache-couchdb-*.tar.gz) -C '${platform}/build' --strip-components=1", label: 'Unpack release' ) dir( "${platform}/build" ) { + withClouseau = meta[platform].with_clouseau ? '-WithClouseau' : '' + powershell( script: """ .\\..\\..\\couchdb-glazier\\bin\\shell.ps1 - .\\configure.ps1 -SkipDeps -WithNouveau -SpiderMonkeyVersion ${meta[platform].spidermonkey_vsn} + .\\configure.ps1 -SkipDeps -WithNouveau ${withClouseau} -SpiderMonkeyVersion ${meta[platform].spidermonkey_vsn} Set-Item -Path env:GRADLE_OPTS -Value '-Dorg.gradle.daemon=false' make -f Makefile.win release """, label: 'Configure and Build') @@ -239,13 +314,17 @@ def generateNativeStage(platform) { //powershell( script: ".\\..\\..\\couchdb-glazier\\bin\\shell.ps1; make -f Makefile.win eunit", label: 'EUnit tests') //powershell( script: ".\\..\\..\\couchdb-glazier\\bin\\shell.ps1; make -f Makefile.win elixir", label: 'Elixir tests') - powershell( script: '& .\\..\\..\\couchdb-glazier\\bin\\shell.ps1; Write-Host "NOT AVAILABLE: make -f Makefile.win elixir-search"', label: 'N/A Clouseau tests') - powershell( script: """ .\\..\\..\\couchdb-glazier\\bin\\shell.ps1 Set-Item -Path env:GRADLE_OPTS -Value '-Dorg.gradle.daemon=false' - make -f Makefile.win mango-test - """, label: 'Mango tests') + make -f Makefile.win elixir-search ERLANG_COOKIE=crumbles + """, label: 'Clouseau tests') + + powershell( script: """ + .\\..\\..\\couchdb-glazier\\bin\\shell.ps1 + Set-Item -Path env:GRADLE_OPTS -Value '-Dorg.gradle.daemon=false' + make -f Makefile.win mango-test ERLANG_COOKIE=crumbles + """, label: 'Mango tests') powershell( script: '.\\..\\..\\couchdb-glazier\\bin\\shell.ps1; Write-Host "NOT AVAILABLE: make -f Makefile.win weatherreport-test"', label: 'N/A Weatherreport tests') @@ -295,17 +374,19 @@ def generateContainerStage(platform) { node(meta[platform].get('node_label', 'docker')) { docker.withRegistry('https://docker.io/', 'dockerhub_creds') { docker.image(meta[platform].image).inside("${DOCKER_ARGS}") { - timeout(time: 90, unit: "MINUTES") { + timeout(time: 180, unit: "MINUTES") { stage("${meta[platform].name} - build & test") { try { sh( script: "rm -rf ${platform} apache-couchdb-*", label: 'Clean workspace' ) unstash 'tarball' sh( script: "mkdir -p ${platform}/build", label: 'Create build directories' ) sh( script: "tar -xf apache-couchdb-*.tar.gz -C ${platform}/build --strip-components=1", label: 'Unpack release' ) + quickjs_tests262 = meta[platform].quickjs_test262 dir( "${platform}/build" ) { sh "${configure(meta[platform])}" sh 'make' retry(3) {sh 'make eunit'} + if (meta[platform].quickjs_test262) {retry(3) {sh 'make quickjs-test262'}} retry(3) {sh 'make elixir'} retry(3) {sh 'make elixir-search'} retry(3) {sh 'make mango-test'} @@ -383,23 +464,161 @@ pipeline { options { buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10')) preserveStashes(buildCount: 10) - timeout(time: 3, unit: 'HOURS') + timeout(time: 4, unit: 'HOURS') timestamps() } stages { + + stage('Setup Env') { + agent { + docker { + image "${DOCKER_IMAGE_BASE}-${MINIMUM_ERLANG_VERSION}" + label 'docker' + args "${DOCKER_ARGS}" + registryUrl 'https://docker.io/' + registryCredentialsId 'dockerhub_creds' + } + } + options { + timeout(time: 10, unit: 'MINUTES') + } + steps { + script { + env.DOCS_CHANGED = '0' + env.ONLY_DOCS_CHANGED = '0' + if ( sh(returnStatus: true, script: docs_changed) == 0 ) { + env.DOCS_CHANGED = '1' + if (sh(returnStatus: true, script: other_changes) == 1) { + env.ONLY_DOCS_CHANGED = '1' + } + } + } + } + post { + cleanup { + // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 + sh 'rm -rf ${WORKSPACE}/*' + } + } + } // stage 'Setup Environment' + + stage('Docs Check') { + // Run docs `make check` stage if any docs changed + when { + beforeOptions true + expression { DOCS_CHANGED == '1' } + } + agent { + docker { + image "${DOCKER_IMAGE_BASE}-${MINIMUM_ERLANG_VERSION}" + label 'docker' + args "${DOCKER_ARGS}" + registryUrl 'https://docker.io/' + registryCredentialsId 'dockerhub_creds' + } + } + options { + timeout(time: 15, unit: 'MINUTES') + } + steps { + sh ''' + make python-black + ''' + sh ''' + (cd src/docs && make check) + ''' + } + post { + cleanup { + // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 + sh 'rm -rf ${WORKSPACE}/*' + } + } + } // stage Docs Check + + stage('Build Docs') { + // Build docs separately if only docs changed. If there are other changes, docs are + // already built as part of `make dist` + when { + beforeOptions true + expression { ONLY_DOCS_CHANGED == '1' } + } + agent { + docker { + image "${DOCKER_IMAGE_BASE}-${MINIMUM_ERLANG_VERSION}" + label 'docker' + args "${DOCKER_ARGS}" + registryUrl 'https://docker.io/' + registryCredentialsId 'dockerhub_creds' + } + } + options { + timeout(time: 30, unit: 'MINUTES') + } + steps { + sh ''' + (cd src/docs && ./setup.sh ; make html) + ''' + } + post { + cleanup { + // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 + sh 'rm -rf ${WORKSPACE}/*' + } + } + } // stage Build Docs + + stage('Source Format Checks') { + when { + beforeOptions true + expression { ONLY_DOCS_CHANGED == '0' } + } + agent { + docker { + image "${DOCKER_IMAGE_BASE}-${MINIMUM_ERLANG_VERSION}" + label 'docker' + args "${DOCKER_ARGS}" + registryUrl 'https://docker.io/' + registryCredentialsId 'dockerhub_creds' + } + } + options { + timeout(time: 15, unit: "MINUTES") + } + steps { + sh ''' + rm -rf apache-couchdb-* + ./configure --skip-deps --spidermonkey-version 78 + make erlfmt-check + make elixir-source-checks + make python-black + ''' + } + post { + cleanup { + // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 + sh 'rm -rf ${WORKSPACE}/*' + } + } + } // stage Erlfmt + stage('Build Release Tarball') { + when { + beforeOptions true + expression { ONLY_DOCS_CHANGED == '0' } + } agent { docker { label 'docker' - image "apache/couchdbci-debian:bookworm-erlang-${MINIMUM_ERLANG_VERSION}" + image "${DOCKER_IMAGE_BASE}-${MINIMUM_ERLANG_VERSION}" args "${DOCKER_ARGS}" registryUrl 'https://docker.io/' registryCredentialsId 'dockerhub_creds' } } steps { - timeout(time: 15, unit: "MINUTES") { + timeout(time: 30, unit: "MINUTES") { sh (script: 'rm -rf apache-couchdb-*', label: 'Clean workspace of any previous release artifacts' ) sh "./configure --spidermonkey-version 78 --with-nouveau" sh 'make dist' @@ -421,10 +640,14 @@ pipeline { } // stage Build Release Tarball stage('Test and Package') { + when { + beforeOptions true + expression { ONLY_DOCS_CHANGED == '0' } + } steps { script { // Including failFast: true in map fails the build immediately if any parallel step fails - parallelStagesMap = meta.collectEntries( [failFast: false] ) { key, values -> + parallelStagesMap = meta.collectEntries( [failFast: true] ) { key, values -> if (values.image) { ["${key}": generateContainerStage(key)] } @@ -436,64 +659,6 @@ pipeline { } } } - - stage('Publish') { - - when { - expression { return env.BRANCH_NAME ==~ /main|2.*.x|3.*.x|4.*.x|jenkins-.*/ } - } - - agent { - docker { - image "apache/couchdbci-debian:bullseye-erlang-${ERLANG_VERSION}" - label 'docker' - args "${DOCKER_ARGS}" - registryUrl 'https://docker.io/' - registryCredentialsId 'dockerhub_creds' - } - } - options { - skipDefaultCheckout() - timeout(time: 90, unit: "MINUTES") - } - - steps { - sh 'rm -rf ${WORKSPACE}/*' - unstash 'tarball' - unarchive mapping: ['pkgs/' : '.'] - - sh( label: 'Setup repo dirs', script: ''' - mkdir -p $BRANCH_NAME/debian $BRANCH_NAME/el8 $BRANCH_NAME/el9 $BRANCH_NAME/source - git clone https://github.com/apache/couchdb-pkg - ''' ) - - sh( label: 'Build Debian repo', script: ''' - for plat in bullseye bookworm focal - do - reprepro -b couchdb-pkg/repo includedeb $plat pkgs/$plat/*.deb - done - ''' ) - - sh( label: 'Build CentOS 8', script: ''' - (cd pkgs/centos8 && createrepo_c --database .) - ''' ) - - sh( label: 'Build CentOS 9', script: ''' - (cd pkgs/centos9 && createrepo_c --database .) - ''' ) - - sh( label: 'Build unified repo', script: ''' - mv couchdb-pkg/repo/pool $BRANCH_NAME/debian - mv couchdb-pkg/repo/dists $BRANCH_NAME/debian - mv pkgs/centos8/* $BRANCH_NAME/el8 - mv pkgs/centos9/* $BRANCH_NAME/el9 - mv apache-couchdb-*.tar.gz $BRANCH_NAME/source - cd $BRANCH_NAME/source - ls -1tr | head -n -10 | xargs -d '\n' rm -f -- - cd ../.. - ''' ) - } // steps - } // stage } // stages post { diff --git a/build-aux/Jenkinsfile.pr b/build-aux/Jenkinsfile.pr deleted file mode 100644 index 09180b17a7..0000000000 --- a/build-aux/Jenkinsfile.pr +++ /dev/null @@ -1,293 +0,0 @@ -#!groovy -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -build = ''' -mkdir -p ${ERLANG_VERSION} -cd ${ERLANG_VERSION} -rm -rf build -mkdir build -cd build -tar -xf ${WORKSPACE}/apache-couchdb-*.tar.gz -cd apache-couchdb-* -./configure --with-nouveau --with-clouseau --js-engine=${JS_ENGINE} -''' - -docs_changed = "git diff --name-only origin/${env.CHANGE_TARGET} | grep -q '^src/docs/'" -other_changes = "git diff --name-only origin/${env.CHANGE_TARGET} | grep -q -v '^src/docs/'" - -pipeline { - - // no top-level agent; agents must be declared for each stage - agent none - - environment { - recipient = 'notifications@couchdb.apache.org' - // Following fix an issue with git <= 2.6.5 where no committer - // name or email are present for reflog, required for git clone - GIT_COMMITTER_NAME = 'Jenkins User' - GIT_COMMITTER_EMAIL = 'couchdb@apache.org' - // Parameters for the matrix build - DOCKER_IMAGE_BASE = 'apache/couchdbci-debian:bookworm-erlang' - // https://github.com/jenkins-infra/jenkins.io/blob/master/Jenkinsfile#64 - // We need the jenkins user mapped inside of the image - // npm config cache below deals with /home/jenkins not mapping correctly - // inside the image - DOCKER_ARGS = '-e npm_config_cache=/home/jenkins/.npm -e HOME=. -e MIX_HOME=/home/jenkins/.mix -e HEX_HOME=/home/jenkins/.hex -e PIP_CACHE_DIR=/home/jenkins/.cache/pip -v=/etc/passwd:/etc/passwd -v /etc/group:/etc/group -v /home/jenkins/.gradle:/home/jenkins/.gradle:rw,z -v /home/jenkins/.hex:/home/jenkins/.hex:rw,z -v /home/jenkins/.npm:/home/jenkins/.npm:rw,z -v /home/jenkins/.cache/pip:/home/jenkins/.cache/pip:rw,z -v /home/jenkins/.mix:/home/jenkins/.mix:rw,z' - - // *** BE SURE TO ALSO CHANGE THE ERLANG VERSIONS FARTHER DOWN *** - // Search for ERLANG_VERSION - // see https://issues.jenkins.io/browse/JENKINS-61047 for why this cannot - // be done parametrically - LOW_ERLANG_VER = '25.3.2.20' - } - - options { - buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10')) - // This fails the build immediately if any parallel step fails - parallelsAlwaysFailFast() - preserveStashes(buildCount: 10) - timeout(time: 3, unit: 'HOURS') - timestamps() - } - - stages { - - stage('Setup Env') { - agent { - docker { - image "${DOCKER_IMAGE_BASE}-${LOW_ERLANG_VER}" - label 'docker' - args "${DOCKER_ARGS}" - registryUrl 'https://docker.io/' - registryCredentialsId 'dockerhub_creds' - } - } - options { - timeout(time: 10, unit: 'MINUTES') - } - steps { - script { - env.DOCS_CHANGED = '0' - env.ONLY_DOCS_CHANGED = '0' - if ( sh(returnStatus: true, script: docs_changed) == 0 ) { - env.DOCS_CHANGED = '1' - if (sh(returnStatus: true, script: other_changes) == 1) { - env.ONLY_DOCS_CHANGED = '1' - } - } - } - } - post { - cleanup { - // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 - sh 'rm -rf ${WORKSPACE}/*' - } - } - } // stage 'Setup Environment' - - stage('Docs Check') { - // Run docs `make check` stage if any docs changed - when { - beforeOptions true - expression { DOCS_CHANGED == '1' } - } - agent { - docker { - image "${DOCKER_IMAGE_BASE}-${LOW_ERLANG_VER}" - label 'docker' - args "${DOCKER_ARGS}" - registryUrl 'https://docker.io/' - registryCredentialsId 'dockerhub_creds' - } - } - options { - timeout(time: 15, unit: 'MINUTES') - } - steps { - sh ''' - make python-black - ''' - sh ''' - (cd src/docs && make check) - ''' - } - post { - cleanup { - // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 - sh 'rm -rf ${WORKSPACE}/*' - } - } - } // stage Docs Check - - stage('Build Docs') { - // Build docs separately if only docs changed. If there are other changes, docs are - // already built as part of `make dist` - when { - beforeOptions true - expression { ONLY_DOCS_CHANGED == '1' } - } - agent { - docker { - image "${DOCKER_IMAGE_BASE}-${LOW_ERLANG_VER}" - label 'docker' - args "${DOCKER_ARGS}" - registryUrl 'https://docker.io/' - registryCredentialsId 'dockerhub_creds' - } - } - options { - timeout(time: 30, unit: 'MINUTES') - } - steps { - sh ''' - (cd src/docs && ./setup.sh ; make html) - ''' - } - post { - cleanup { - // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 - sh 'rm -rf ${WORKSPACE}/*' - } - } - } // stage Build Docs - - stage('Source Format Checks') { - when { - beforeOptions true - expression { ONLY_DOCS_CHANGED == '0' } - } - agent { - docker { - image "${DOCKER_IMAGE_BASE}-${LOW_ERLANG_VER}" - label 'docker' - args "${DOCKER_ARGS}" - registryUrl 'https://docker.io/' - registryCredentialsId 'dockerhub_creds' - } - } - options { - timeout(time: 15, unit: "MINUTES") - } - steps { - sh ''' - rm -rf apache-couchdb-* - ./configure --skip-deps --spidermonkey-version 78 - make erlfmt-check - make elixir-source-checks - make python-black - ''' - } - post { - cleanup { - // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 - sh 'rm -rf ${WORKSPACE}/*' - } - } - } // stage Erlfmt - - - stage('Make Dist') { - when { - beforeOptions true - expression { ONLY_DOCS_CHANGED == '0' } - } - agent { - docker { - image "${DOCKER_IMAGE_BASE}-${LOW_ERLANG_VER}" - label 'docker' - args "${DOCKER_ARGS}" - registryUrl 'https://docker.io/' - registryCredentialsId 'dockerhub_creds' - } - } - options { - timeout(time: 15, unit: "MINUTES") - } - steps { - sh ''' - rm -rf apache-couchdb-* - ./configure --spidermonkey-version 78 --with-nouveau --with-clouseau - make dist - chmod -R a+w * . - ''' - } - post { - success { - stash includes: 'apache-couchdb-*.tar.gz', name: 'tarball' - } - cleanup { - // UGH see https://issues.jenkins-ci.org/browse/JENKINS-41894 - sh 'rm -rf ${WORKSPACE}/*' - } - } - } // stage Make Dist - - // TODO Rework once Improved Docker Pipeline Engine is released - // https://issues.jenkins-ci.org/browse/JENKINS-47962 - // https://issues.jenkins-ci.org/browse/JENKINS-48050 - - stage('Make Check') { - when { - beforeOptions true - expression { ONLY_DOCS_CHANGED == '0' } - } - matrix { - axes { - axis { - name 'ERLANG_VERSION' - values '25.3.2.20', '26.2.5.11', '27.3.3' - } - axis { - name 'SM_VSN' - values '78' - } - axis { - name 'JS_ENGINE' - values 'quickjs', 'spidermonkey' - } - } - - stages { - stage('Build and Test') { - agent { - docker { - image "${DOCKER_IMAGE_BASE}-${ERLANG_VERSION}" - label 'docker' - args "${DOCKER_ARGS}" - } - } - options { - skipDefaultCheckout() - timeout(time: 90, unit: "MINUTES") - } - steps { - unstash 'tarball' - sh( script: build ) - retry(3) {sh 'cd ${ERLANG_VERSION}/build/apache-couchdb-* && make check || (make build-report && false)'} - } - post { - always { - junit '**/.eunit/*.xml, **/_build/*/lib/couchdbtest/*.xml, **/src/mango/nosetests.xml, **/test/javascript/junit.xml' - } - cleanup { - sh 'rm -rf ${WORKSPACE}/*' - } - } - } // stage - } // stages - } // matrix - } // stage "Make Check" - } // stages -} // pipeline diff --git a/build-aux/xref-helper.sh b/build-aux/xref-helper.sh index 5e1c3ed1ab..2b83c74f31 100755 --- a/build-aux/xref-helper.sh +++ b/build-aux/xref-helper.sh @@ -9,7 +9,6 @@ mkdir -p ./tmp $REBAR --keep-going --recursive xref $DIALYZE_OPTS | \ grep -v '==>' | \ grep -v 'WARN' | \ - grep -v hastings | \ sort > ./tmp/xref-output.txt # compare result against known allowed output diff --git a/configure b/configure index 70350a07b5..30ad40c24a 100755 --- a/configure +++ b/configure @@ -21,8 +21,8 @@ basename=`basename $0` PACKAGE_AUTHOR_NAME="The Apache Software Foundation" -REBAR3_BRANCH="3.23.0" -ERLFMT_VERSION="v1.3.0" +REBAR3_BRANCH="3.25.1" +ERLFMT_VERSION="v1.7.0" # TEST=0 WITH_PROPER="true" @@ -43,9 +43,10 @@ JS_ENGINE=${JS_ENGINE:-"spidermonkey"} SM_VSN=${SM_VSN:-"91"} CLOUSEAU_MTH=${CLOUSEAU_MTH:-"dist"} CLOUSEAU_URI=${CLOUSEAU_URI:-"https://github.com/cloudant-labs/clouseau/releases/download/%s/clouseau-%s-dist.zip"} -CLOUSEAU_VSN=${CLOUSEAU_VSN:-"2.23.1"} +CLOUSEAU_VSN=${CLOUSEAU_VSN:-"2.25.0"} CLOUSEAU_DIR="$(pwd)"/clouseau ARCH="$(uname -m)" +MULTIARCH_NAME="$(command -v dpkg-architecture > /dev/null && dpkg-architecture -q DEB_HOST_MULTIARCH || true)" ERLANG_VER="$(run_erlang 'io:put_chars(erlang:system_info(otp_release)).')" ERLANG_OS="$(run_erlang 'case os:type() of {OS, _} -> io:format("~s~n", [OS]) end.')" @@ -126,6 +127,7 @@ parse_opts() { --dev) WITH_DOCS="false" WITH_FAUXTON="false" + WITH_SPIDERMONKEY="false" shift continue ;; @@ -134,6 +136,7 @@ parse_opts() { WITH_DOCS="false" WITH_FAUXTON="false" WITH_NOUVEAU="true" + WITH_SPIDERMONKEY="false" shift continue ;; @@ -327,6 +330,7 @@ if [ "${WITH_SPIDERMONKEY}" = "true" ] && [ "${ERLANG_OS}" = "unix" ]; then # This list is taken from src/couch/rebar.config.script, please keep them in sync. if [ ! -d "/usr/include/${SM_HEADERS}" ] && \ + [ -n "${MULTIARCH_NAME}" -a ! -d "/usr/include/${MULTIARCH_NAME}/${SM_HEADERS}" ] && \ [ ! -d "/usr/local/include/${SM_HEADERS}" ] && \ [ ! -d "/opt/homebrew/include/${SM_HEADERS}" ]; then echo "ERROR: SpiderMonkey ${SM_VSN} is not found. Please specify with --spidermonkey-version." diff --git a/configure.ps1 b/configure.ps1 index faf3669ece..c7c730c70d 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -14,9 +14,9 @@ -CouchDBUser USER set the username to run as (defaults to current user) -SpiderMonkeyVersion VSN select the version of SpiderMonkey to use (default 91) -JSEngine ENGINE select JS engine to use (spidermonkey or quickjs) (default spidermonkey) - -ClouseauVersion VSN select the version of Clouseau to use (default 2.23.1) + -ClouseauVersion VSN select the version of Clouseau to use (default 2.25.0) -ClouseauMethod MTH method for Clouseau to deploy: git or dist (default dist) - -ClouseauUri URI location for retrieving Clouseau (default https://github.com/cloudant-labs/clouseau/releases/download/2.23.1/clouseau-2.23.1-dist.zip) + -ClouseauUri URI location for retrieving Clouseau (default https://github.com/cloudant-labs/clouseau/releases/download/2.25.0/clouseau-2.25.0-dist.zip) Installation directories: -Prefix PREFIX install architecture-independent files in PREFIX @@ -66,9 +66,9 @@ Param( [ValidateNotNullOrEmpty()] [string]$ClouseauMethod = "dist", # method for Clouseau to deploy: git or dist (default dist) [ValidateNotNullOrEmpty()] - [string]$ClouseauVersion = "2.23.1", # select the version of Clouseau to use (default 2.23.1) + [string]$ClouseauVersion = "2.25.0", # select the version of Clouseau to use (default 2.25.0) [ValidateNotNullOrEmpty()] - [string]$ClouseauUri = "https://github.com/cloudant-labs/clouseau/releases/download/{0}/clouseau-{0}-dist.zip", # location for retrieving Clouseau (default https://github.com/cloudant-labs/clouseau/releases/download/2.23.1/clouseau-2.23.1-dist.zip) + [string]$ClouseauUri = "https://github.com/cloudant-labs/clouseau/releases/download/{0}/clouseau-{0}-dist.zip", # location for retrieving Clouseau (default https://github.com/cloudant-labs/clouseau/releases/download/2.25.0/clouseau-2.25.0-dist.zip) [ValidateNotNullOrEmpty()] [string]$Prefix = "C:\Program Files\Apache\CouchDB", # install architecture-independent file location (default C:\Program Files\Apache\CouchDB) [ValidateNotNullOrEmpty()] diff --git a/dev/run b/dev/run index 85ea9622d5..478fb2ae9a 100755 --- a/dev/run +++ b/dev/run @@ -515,7 +515,7 @@ def write_config(ctx, node, env): content = apply_config_overrides(ctx, content) elif base == "local.ini": content = hack_local_ini(ctx, content) - elif ctx["enable_tls"] and base == "vm.args": + elif base == "vm.args": content = hack_vm_args(ctx, node, content) with open(tgt, "w") as handle: @@ -839,16 +839,23 @@ def hack_local_ini(ctx, contents): def hack_vm_args(ctx, node, contents): - contents += f""" + if ctx["enable_tls"]: + contents += f""" -proto_dist couch -couch_dist no_tls '"clouseau{node[-1]}@127.0.0.1"' -ssl_dist_optfile {ctx["rootdir"]}/src/couch_dist/certs/out/couch_dist.conf - """ - if ctx["no_tls"]: - no_tls_nodes = ctx["no_tls"].split(",") - for node_name in no_tls_nodes: - node_name = node_name if "@" in node_name else f"{node_name}@127.0.0.1" - contents += f"""\n-couch_dist no_tls '"{node_name}"'""" +""" + if ctx["no_tls"]: + no_tls_nodes = ctx["no_tls"].split(",") + for node_name in no_tls_nodes: + node_name = node_name if "@" in node_name else f"{node_name}@127.0.0.1" + contents += f""" +-couch_dist no_tls '"{node_name}"' +""" + if ctx["erlang_cookie"]: + contents += f""" +-setcookie {ctx["erlang_cookie"]} +""" return contents diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/api/Ok.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/Ok.java new file mode 100644 index 0000000000..b393e1978a --- /dev/null +++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/Ok.java @@ -0,0 +1,26 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.couchdb.nouveau.api; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Ok { + + public static final Ok INSTANCE = new Ok(); + + @JsonProperty + public boolean ok() { + return true; + } +} diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java index 85230d3223..c4f59f7a15 100644 --- a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java +++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java @@ -33,7 +33,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.stream.Stream; import org.apache.couchdb.nouveau.api.IndexDefinition; import org.apache.couchdb.nouveau.lucene9.Lucene9AnalyzerFactory; import org.apache.couchdb.nouveau.lucene9.Lucene9Index; @@ -250,35 +249,49 @@ public boolean exists(final String name) { } public void deleteAll(final String path, final List exclusions) throws IOException { - LOGGER.info("deleting indexes below {} (excluding {})", path, exclusions == null ? "nothing" : exclusions); + LOGGER.info( + "deleting indexes matching {} (excluding {})", + path, + exclusions == null || exclusions.isEmpty() ? "nothing" : exclusions); + var parts = path.split("/"); + deleteAll(rootDir, parts, 0, exclusions); + } - final Path indexRootPath = indexRootPath(path); - if (!indexRootPath.toFile().exists()) { + private void deleteAll(final Path path, final String[] parts, final int index, final List exclusions) + throws IOException { + // End of the path + if (index == parts.length - 1) { + try (var stream = Files.newDirectoryStream(path, parts[index])) { + stream.forEach(p -> { + if (exclusions != null && exclusions.indexOf(p.getFileName().toString()) != -1) { + return; + } + final String relativeName = rootDir.relativize(p).toString(); + try { + deleteIndex(relativeName); + } catch (final IOException | InterruptedException e) { + LOGGER.error("Exception deleting {}", p, e); + } + // Clean any newly empty directories. + do { + final File f = p.toFile(); + if (f.isDirectory() && f.list().length == 0) { + f.delete(); + } + } while ((p = p.getParent()) != null && !rootDir.equals(p)); + }); + } return; } - Stream stream = Files.find(indexRootPath, 100, (p, attr) -> attr.isDirectory() && isIndex(p)); - try { - stream.forEach((p) -> { - final String relativeToExclusions = indexRootPath.relativize(p).toString(); - if (exclusions != null && exclusions.indexOf(relativeToExclusions) != -1) { - return; - } - final String relativeName = rootDir.relativize(p).toString(); + // Recurse + try (var stream = Files.newDirectoryStream(path, parts[index])) { + stream.forEach(p -> { try { - deleteIndex(relativeName); - } catch (final IOException | InterruptedException e) { - LOGGER.error("Exception deleting {}", p, e); + deleteAll(p, parts, index + 1, exclusions); + } catch (IOException e) { + LOGGER.warn("Exception during delete of " + rootDir.relativize(p), e); } - // Clean any newly empty directories. - do { - final File f = p.toFile(); - if (f.isDirectory() && f.list().length == 0) { - f.delete(); - } - } while ((p = p.getParent()) != null && !rootDir.equals(p)); }); - } finally { - stream.close(); } } diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java index a6ca2c47b6..a52e00da91 100644 --- a/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java +++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java @@ -35,6 +35,7 @@ import org.apache.couchdb.nouveau.api.IndexDefinition; import org.apache.couchdb.nouveau.api.IndexInfo; import org.apache.couchdb.nouveau.api.IndexInfoRequest; +import org.apache.couchdb.nouveau.api.Ok; import org.apache.couchdb.nouveau.api.SearchRequest; import org.apache.couchdb.nouveau.api.SearchResults; import org.apache.couchdb.nouveau.core.IndexManager; @@ -54,27 +55,29 @@ public IndexResource(final IndexManager indexManager) { } @PUT - public void createIndex(@PathParam("name") String name, @NotNull @Valid IndexDefinition indexDefinition) + public Ok createIndex(@PathParam("name") String name, @NotNull @Valid IndexDefinition indexDefinition) throws IOException { indexManager.create(name, indexDefinition); + return Ok.INSTANCE; } @DELETE @Path("/doc/{docId}") - public void deleteDoc( + public Ok deleteDoc( @PathParam("name") String name, @PathParam("docId") String docId, @NotNull @Valid DocumentDeleteRequest request) throws Exception { - indexManager.with(name, (index) -> { + return indexManager.with(name, (index) -> { index.delete(docId, request); - return null; + return Ok.INSTANCE; }); } @DELETE - public void deletePath(@PathParam("name") String path, @Valid final List exclusions) throws IOException { + public Ok deletePath(@PathParam("name") String path, @Valid final List exclusions) throws IOException { indexManager.deleteAll(path, exclusions); + return Ok.INSTANCE; } @GET @@ -85,9 +88,8 @@ public IndexInfo getIndexInfo(@PathParam("name") String name) throws Exception { } @POST - public void setIndexInfo(@PathParam("name") String name, @NotNull @Valid IndexInfoRequest request) - throws Exception { - indexManager.with(name, (index) -> { + public Ok setIndexInfo(@PathParam("name") String name, @NotNull @Valid IndexInfoRequest request) throws Exception { + return indexManager.with(name, (index) -> { if (request.getMatchUpdateSeq().isPresent() && request.getUpdateSeq().isPresent()) { index.setUpdateSeq( @@ -99,7 +101,7 @@ public void setIndexInfo(@PathParam("name") String name, @NotNull @Valid IndexIn request.getMatchPurgeSeq().getAsLong(), request.getPurgeSeq().getAsLong()); } - return null; + return Ok.INSTANCE; }); } @@ -114,14 +116,14 @@ public SearchResults searchIndex(@PathParam("name") String name, @NotNull @Valid @PUT @Path("/doc/{docId}") - public void updateDoc( + public Ok updateDoc( @PathParam("name") String name, @PathParam("docId") String docId, @NotNull @Valid DocumentUpdateRequest request) throws Exception { - indexManager.with(name, (index) -> { + return indexManager.with(name, (index) -> { index.update(docId, request); - return null; + return Ok.INSTANCE; }); } } diff --git a/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java b/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java index a994cdd0c2..bb8f0b6470 100644 --- a/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java +++ b/nouveau/src/test/java/org/apache/couchdb/nouveau/core/IndexManagerTest.java @@ -16,7 +16,10 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ScheduledExecutorService; @@ -24,19 +27,21 @@ import org.apache.couchdb.nouveau.api.IndexDefinition; import org.apache.couchdb.nouveau.api.SearchRequest; import org.apache.couchdb.nouveau.lucene9.ParallelSearcherFactory; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; public class IndexManagerTest { - private static IndexManager manager; - private static ScheduledExecutorService executorService; + private Path rootDir; + private IndexManager manager; + private ScheduledExecutorService executorService; - @BeforeAll - public static void setupManager(@TempDir Path path) throws Exception { + @BeforeEach + public void setupManager(@TempDir Path path) throws Exception { executorService = Executors.newScheduledThreadPool(2); + rootDir = path; manager = new IndexManager(); manager.setRootDir(path); @@ -47,8 +52,8 @@ public static void setupManager(@TempDir Path path) throws Exception { manager.start(); } - @AfterAll - public static void cleanup() throws Exception { + @AfterEach + public void cleanup() throws Exception { executorService.shutdownNow(); executorService.awaitTermination(5, TimeUnit.SECONDS); manager.stop(); @@ -82,4 +87,60 @@ public void managerReopensAClosedIndex() throws Exception { }); assertThat(isOpen); } + + @Test + public void deleteAllRemovesIndexByName() throws Exception { + final IndexDefinition indexDefinition = new IndexDefinition(); + indexDefinition.setDefaultAnalyzer("standard"); + + assertThat(countIndexes()).isEqualTo(0); + manager.create("bar", indexDefinition); + assertThat(countIndexes()).isEqualTo(1); + manager.deleteAll("bar", null); + assertThat(countIndexes()).isEqualTo(0); + } + + @Test + public void deleteAllRemovesIndexByPath() throws Exception { + final IndexDefinition indexDefinition = new IndexDefinition(); + indexDefinition.setDefaultAnalyzer("standard"); + + assertThat(countIndexes()).isEqualTo(0); + manager.create("foo/bar", indexDefinition); + assertThat(countIndexes()).isEqualTo(1); + manager.deleteAll("foo/bar", null); + assertThat(countIndexes()).isEqualTo(0); + } + + @Test + public void deleteAllRemovesIndexByGlob() throws Exception { + final IndexDefinition indexDefinition = new IndexDefinition(); + indexDefinition.setDefaultAnalyzer("standard"); + + assertThat(countIndexes()).isEqualTo(0); + manager.create("foo/bar", indexDefinition); + assertThat(countIndexes()).isEqualTo(1); + manager.deleteAll("foo/*", null); + assertThat(countIndexes()).isEqualTo(0); + } + + @Test + public void deleteAllRemovesIndexByGlobExceptExclusions() throws Exception { + final IndexDefinition indexDefinition = new IndexDefinition(); + indexDefinition.setDefaultAnalyzer("standard"); + + assertThat(countIndexes()).isEqualTo(0); + manager.create("foo/bar", indexDefinition); + manager.create("foo/baz", indexDefinition); + assertThat(countIndexes()).isEqualTo(2); + manager.deleteAll("foo/*", List.of("bar")); + assertThat(countIndexes()).isEqualTo(1); + } + + private long countIndexes() throws IOException { + try (var stream = + Files.find(rootDir, 10, (p, attr) -> p.getFileName().toString().equals("index_definition.json"))) { + return stream.count(); + } + } } diff --git a/rebar.config.script b/rebar.config.script index 57fa8fae6b..efc03a35e6 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -150,17 +150,20 @@ SubDirs = [ "rel" ]. +% Try to keep deps in sync with .elp.toml deps + DepDescs = [ %% Independent Apps {snappy, "snappy", {tag, "CouchDB-1.0.9"}}, %% %% Non-Erlang deps {fauxton, {url, "https://github.com/apache/couchdb-fauxton"}, - {tag, "v1.3.4"}, [raw]}, + {tag, "v1.3.5"}, [raw]}, {ibrowse, "ibrowse", {tag, "CouchDB-4.4.2-6"}}, +{gun, "gun", {tag, "2.2.0-couchdb"}}, {jiffy, "jiffy", {tag, "1.1.2"}}, -{mochiweb, "mochiweb", {tag, "v3.2.2"}}, -{meck, "meck", {tag, "1.0.0"}}, +{mochiweb, "mochiweb", {tag, "v3.3.0"}}, +{meck, "meck", {tag, "v1.1.0"}}, {recon, "recon", {tag, "2.5.6"}} ]. @@ -168,7 +171,7 @@ WithProper = lists:keyfind(with_proper, 1, CouchConfig) == {with_proper, true}. OptionalDeps = case WithProper of true -> - [{proper, {url, "https://github.com/proper-testing/proper"}, "f2a44ee11a238c84403e72ee8ec68e6f65fd7e42"}]; + [{proper, {url, "https://github.com/proper-testing/proper"}, "v1.5.0"}]; false -> [] end. @@ -191,7 +194,7 @@ end. AddConfig = [ {cover_enabled, true}, {cover_print_enabled, true}, - {require_otp_vsn, "25|26|27|28"}, + {require_otp_vsn, "26|27|28"}, {deps_dir, "src"}, {deps, lists:map(MakeDep, DepDescs ++ OptionalDeps)}, {sub_dirs, SubDirs}, diff --git a/rel/nouveau.yaml b/rel/nouveau.yaml index 40837f12cb..0f33d5f25c 100644 --- a/rel/nouveau.yaml +++ b/rel/nouveau.yaml @@ -8,12 +8,12 @@ logging: server: applicationConnectors: - - type: http + - type: h2c bindHost: 127.0.0.1 port: {{nouveau_port}} useDateHeader: false adminConnectors: - - type: http + - type: h2c bindHost: 127.0.0.1 port: {{nouveau_admin_port}} useDateHeader: false diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index dfefa62dc0..00d92bafe3 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -71,11 +71,18 @@ view_index_dir = {{view_index_dir}} ;default_engine = couch ; Enable this to only "soft-delete" databases when DELETE /{db} requests are -; made. This will place a .recovery directory in your data directory and -; move deleted databases/shards there instead. You can then manually delete -; these files later, as desired. +; made. This will add `.deleted.couch` extension to the database shard files +; in the data directory. You can then manually delete these files later, as +; desired. ;enable_database_recovery = false +; Applies only when `enable_database_recovery = false`. +; When DELETE /{db} requests are made: +; - If `true`, delete the database shard files from the `data` directory. +; - If `false`, rename the database shard files and move them to the `.delete` +; directory. +;delete_after_rename = true + ; Set the maximum size allowed for a partition. This helps users avoid ; inadvertently abusing partitions resulting in hot shards. The default ; is 10GiB. A value of 0 or less will disable partition size checks. @@ -116,16 +123,43 @@ view_index_dir = {{view_index_dir}} ; When enabled, use cfile parallel reads for all the requests. By default the ; setting is "false", so only requests which are configured to bypass the IOQ ; would use the cfile parallel reads. If there is enough RAM available for a -; large file cache and the disks have enough IO bandwith, consider enabling +; large file cache and the disks have enough IO bandwidth, consider enabling ; this setting. ;cfile_skip_ioq = false -[purge] -; Allowed maximum number of documents in one purge request -;max_document_id_number = 100 +; CouchDB will not normally allow a database downgrade to a previous version if +; the new version added new fields to the database file header structure. You +; can disable this protection by setting this to false. We recommend +; re-enabling the protection after you have performed the downgrade but it is +; not mandatory to do so. +; Note that some future versions of CouchDB might not support downgrade at all, +; whatever value this is set to. In those cases CouchDB will refuse to +; downgrade or even open the databases in question. +;prohibit_downgrade = true + +[bt_engine_cache] +; Memory used for btree engine cache. This is a cache for top levels of +; database btrees (id tree, seq tree) and a few terms from the db header. Value +; is in bytes. +;max_size = 67108864 +; +; Items not accessed in a while are eventually evicted. However, if the memory +; used is below this percentage, then even the unused items are left in the +; cache. The trade-off here is when a new workload starts, it may find the +; cache with some stale items during the first few seconds and not be able to +; insert its entries in. +;leave_percent = 30 +; +; Cache database btree nodes up to this depth only. Depth starts at 1 at root, +; then at 2 the next level down, and so on. Only intermediate (pointer) nodes +; will be cached, those are the nodes which point to other nodes, as opposed to +; leaf key-value nodes, which hold revision trees. To disable db btree node +; caching set the value to 0 +;db_btree_cache_depth = 3 +[purge] ; Allowed maximum number of accumulated revisions in one purge request -;max_revisions_number = 1000 +;max_revisions_number = infinity ; Allowed durations when index is not updated for local purge checkpoint ; document. Default is 24 hours. @@ -217,7 +251,7 @@ bind_address = 127.0.0.1 ; Set to false to revert to a previous _bulk_get implementation using single ; doc fetches internally. Using batches should be faster, however there may be -; bugs in the new new implemention, so expose this option to allow reverting to +; bugs in the new implementation, so expose this option to allow reverting to ; the old behavior. ;bulk_get_use_batches = true @@ -401,7 +435,8 @@ hash_algorithms = sha256, sha ;uuid_prefix_len = 7 ;request_timeout = 60000 ;all_docs_timeout = 10000 -;attachments_timeout = 60000 +;all_docs_view_permsg_timeout = 5000 +;attachments_timeout = 600000 ;view_timeout = infinity ;view_permsg_timeout = 3600000 ;partition_view_timeout = infinity @@ -428,6 +463,16 @@ hash_algorithms = sha256, sha ;update_db = true ;[view_updater] +; Configure the queue capacity used during indexing. These settings apply to +; both the queue between the changes feed and the JS mapper, and between the +; JS mapper and the disk writer. +; Whichever limit happens to be hit first is the one that takes effect. + +; The maximum queue memory size +;queue_memory_cap = 100000 +; The maximum queue length +;queue_item_cap = 500 + ;min_writer_items = 100 ;min_writer_size = 16777216 @@ -500,12 +545,19 @@ authentication_db = _users ; Erlang Query Server ;enable_erlang_query_server = false -; Changing reduce_limit to false will disable reduce_limit. -; If you think you're hitting reduce_limit with a "good" reduce function, -; please let us know on the mailing list so we can fine tune the heuristic. + [query_server_config] ;commit_freq = 5 +; Changing reduce_limit to false will disable reduce_limit. Setting the reduce +; limit to log will only log a warning instead of crashing the view. If you +; think you're hitting reduce_limit with a "good" reduce function, please let +; us know on the mailing list so we can fine tune the heuristic. ;reduce_limit = true +; Don't log/crash a reduce if the result is less than this number of bytes; +;reduce_limit_threshold = 5000 +; Don't log/crash a reduce if the result is less than the ratio multiplied +; by input size +;reduce_limit_ratio = 2.0 ;os_process_limit = 100 ;os_process_idle_limit = 300 ;os_process_soft_limit = 100 @@ -659,13 +711,17 @@ partitioned||* = true ; Maximum peer certificate depth (must be set even if certificate validation is off). ;ssl_certificate_max_depth = 3 +; How often to reload operating system CA certificates (in hours). The default +; is 24 hours. +;cacert_reload_interval_hours = 24 + ; Maximum document ID length for replication. ;max_document_id_length = infinity ; How much time to wait before retrying after a missing doc exception. This ; exception happens if the document was seen in the changes feed, but internal ; replication hasn't caught up yet, and fetching document's revisions -; fails. This a common scenario when source is updated while continuous +; fails. This is a common scenario when source is updated while continuous ; replication is running. The retry period would depend on how quickly internal ; replication is expected to catch up. In general this is an optimisation to ; avoid crashing the whole replication job, which would consume more resources @@ -684,7 +740,7 @@ partitioned||* = true ; couch_replicator_auth_session - use _session cookie authentication ; couch_replicator_auth_noop - use basic authentication (previous default) ; Currently, the new _session cookie authentication is tried first, before -; falling back to the old basic authenticaion default: +; falling back to the old basic authentication default: ;auth_plugins = couch_replicator_auth_session,couch_replicator_auth_noop ; To restore the old behaviour, use the following value: @@ -712,7 +768,7 @@ partitioned||* = true ; priority 0, and would render this algorithm useless. The default value of ; 0.98 is picked such that if a job ran for one scheduler cycle, then didn't ; get to run for 7 hours, it would still have priority > 0. 7 hours was picked -; as it was close enought to 8 hours which is the default maximum error backoff +; as it was close enough to 8 hours which is the default maximum error backoff ; interval. ;priority_coeff = 0.98 @@ -925,12 +981,11 @@ port = {{prometheus_port}} [custodian] ; When set to `true`, force using `[cluster] n` values as the expected n value -; of of shard copies. In cases where the application prevents creating -; non-default n databases, this could help detect case where the shard map was -; altered by hand, or via an external tools, such that it doesn't have the -; necessary number of copies for some ranges. By default, when the setting is -; `false`, the expected n value is based on the number of available copies in -; the shard map. +; of shard copies. In cases where the application prevents creating non-default +; n databases, this could help detect case where the shard map was altered by +; hand, or via an external tools, such that it doesn't have the necessary number +; of copies for some ranges. By default, when the setting is `false`, the +; expected n value is based on the number of available copies in the shard map. ;use_cluster_n_as_expected_n = false [nouveau] @@ -1001,6 +1056,17 @@ url = {{nouveau_url}} ; is shared across all running plugins. ;doc_rate_limit = 1000 +; Limit the rate per second at which plugins write/update documents. The rate +; is shared across all running plugins. Unlike other rate limit which are +; applied automatically by the plugin backend this rate assume the plugins will +; explicitly use the couch_scanner_rate_limiter API when performing writes. +;doc_write_rate_limit = 500 + +; Batch size to use when fetching design documents. For lots of small design +; documents this value could be increased to 500 or 1000. If design documents +; are large (100KB+) it could make sense to decrease it a bit to 25 or 10. +;ddoc_batch_size = 100 + [couch_scanner_plugins] ;couch_scanner_plugin_ddoc_features = false ;couch_scanner_plugin_find = false diff --git a/rel/overlay/etc/vm.args b/rel/overlay/etc/vm.args index d762c33bf2..b4167be7b5 100644 --- a/rel/overlay/etc/vm.args +++ b/rel/overlay/etc/vm.args @@ -57,6 +57,17 @@ # -kernel prevent_overlapping_partitions false +# Set Erlang process limit. If not set in Erlang 27 and greater versions it +# defaults to 1048576. In Erlang versions less than 27 it defaulted to 262144. +# When a cluster reaches this limit it will emit "Too many processes" error in +# the log. That could be a time to bump it up. The actual value set will be a +# larger power of 2 number. To check the actual limit call +# `erlang:system_info(process_limit).` in remsh. +# +# For additional info see +# https://www.erlang.org/doc/apps/erts/erl_cmd.html#emulator-flags ++P 1048576 + # Increase the pool of dirty IO schedulers from 10 to 16 # Dirty IO schedulers are used for file IO. +SDio 16 diff --git a/rel/reltool.config b/rel/reltool.config index c1f0ea0706..b85bd49b62 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -48,6 +48,8 @@ ets_lru, fabric, global_changes, + gun, + cowlib, ibrowse, ioq, jiffy, @@ -111,6 +113,8 @@ {app, ets_lru, [{incl_cond, include}]}, {app, fabric, [{incl_cond, include}]}, {app, global_changes, [{incl_cond, include}]}, + {app, gun, [{incl_cond, include}]}, + {app, cowlib, [{incl_cond, include}]}, {app, ibrowse, [{incl_cond, include}]}, {app, ioq, [{incl_cond, include}]}, {app, jiffy, [{incl_cond, include}]}, diff --git a/share/server/views.js b/share/server/views.js index b59c9912e8..623b5c4b5b 100644 --- a/share/server/views.js +++ b/share/server/views.js @@ -36,9 +36,9 @@ var Views = (function() { var reduce_line = JSON.stringify(reductions); var reduce_length = reduce_line.length; var input_length = State.line_length - code_size - // TODO make reduce_limit config into a number if (State.query_config && State.query_config.reduce_limit && - reduce_length > 4096 && ((reduce_length * 2) > input_length)) { + reduce_length > State.query_config.reduce_limit_threshold && + ((reduce_length * State.query_config.reduce_limit_ratio) > input_length)) { var log_message = [ "Reduce output must shrink more rapidly:", "input size:", input_length, diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl index a43baeae48..d0f5e01110 100644 --- a/src/chttpd/src/chttpd_db.erl +++ b/src/chttpd/src/chttpd_db.erl @@ -454,8 +454,7 @@ delete_db_req(#httpd{} = Req, DbName) -> end. do_db_req(#httpd{path_parts = [DbName | _], user_ctx = Ctx} = Req, Fun) -> - Shard = hd(mem3:shards(DbName)), - Props = couch_util:get_value(props, Shard#shard.opts, []), + Props = mem3:props(DbName), Opts = case Ctx of undefined -> @@ -695,22 +694,17 @@ db_req(#httpd{method = 'POST', path_parts = [_, <<"_purge">>]} = Req, Db) -> Options = [{user_ctx, Req#httpd.user_ctx}, {w, W}], {IdsRevs} = chttpd:json_body_obj(Req), IdsRevs2 = [{Id, couch_doc:parse_revs(Revs)} || {Id, Revs} <- IdsRevs], - MaxIds = config:get_integer("purge", "max_document_id_number", 100), - case length(IdsRevs2) =< MaxIds of - false -> throw({bad_request, "Exceeded maximum number of documents."}); - true -> ok - end, - RevsLen = lists:foldl( - fun({_Id, Revs}, Acc) -> - length(Revs) + Acc - end, - 0, - IdsRevs2 - ), - MaxRevs = config:get_integer("purge", "max_revisions_number", 1000), - case RevsLen =< MaxRevs of - false -> throw({bad_request, "Exceeded maximum number of revisions."}); - true -> ok + case config:get("purge", "max_revisions_number", "infinity") of + "infinity" -> + ok; + Val -> + MaxRevs = list_to_integer(Val), + SumFun = fun({_Id, Revs}, Acc) -> length(Revs) + Acc end, + RevsLen = lists:foldl(SumFun, 0, IdsRevs2), + case RevsLen =< MaxRevs of + false -> throw({bad_request, "Exceeded maximum number of revisions."}); + true -> ok + end end, couch_stats:increment_counter([couchdb, document_purges, total], length(IdsRevs2)), Results2 = @@ -1419,7 +1413,7 @@ receive_request_data(Req, Len) when Len == chunked -> GetChunk }; receive_request_data(Req, LenLeft) when LenLeft > 0 -> - Len = erlang:min(4096, LenLeft), + Len = min(4096, LenLeft), Data = chttpd:recv(Req, Len), {Data, fun() -> receive_request_data(Req, LenLeft - iolist_size(Data)) end}; receive_request_data(_Req, _) -> @@ -1460,13 +1454,13 @@ purge_results_to_json([{DocId, _Revs} | RIn], [{accepted, PRevs} | ROut]) -> {Code, Results} = purge_results_to_json(RIn, ROut), couch_stats:increment_counter([couchdb, document_purges, success]), NewResults = [{DocId, couch_doc:revs_to_strs(PRevs)} | Results], - {erlang:max(Code, 202), NewResults}; + {max(Code, 202), NewResults}; purge_results_to_json([{DocId, _Revs} | RIn], [Error | ROut]) -> {Code, Results} = purge_results_to_json(RIn, ROut), {NewCode, ErrorStr, Reason} = chttpd:error_info(Error), couch_stats:increment_counter([couchdb, document_purges, failure]), NewResults = [{DocId, {[{error, ErrorStr}, {reason, Reason}]}} | Results], - {erlang:max(NewCode, Code), NewResults}. + {max(NewCode, Code), NewResults}. send_updated_doc(Req, Db, DocId, Json) -> send_updated_doc(Req, Db, DocId, Json, []). @@ -1526,9 +1520,9 @@ update_doc(Db, DocId, #doc{deleted = Deleted, body = DocBody} = Doc, Options) -> {'DOWN', Ref, _, _, {exit_throw, Reason}} -> throw(Reason); {'DOWN', Ref, _, _, {exit_error, Reason}} -> - erlang:error(Reason); + error(Reason); {'DOWN', Ref, _, _, {exit_exit, Reason}} -> - erlang:exit(Reason) + exit(Reason) end, case Result of diff --git a/src/chttpd/src/chttpd_external.erl b/src/chttpd/src/chttpd_external.erl index 4cd1d996fe..fe63de3b22 100644 --- a/src/chttpd/src/chttpd_external.erl +++ b/src/chttpd/src/chttpd_external.erl @@ -139,7 +139,7 @@ to_json_terms(Data) -> to_json_terms([], Acc) -> {lists:reverse(Acc)}; to_json_terms([{Key, Value} | Rest], Acc) when is_atom(Key) -> - to_json_terms(Rest, [{list_to_binary(atom_to_list(Key)), list_to_binary(Value)} | Acc]); + to_json_terms(Rest, [{atom_to_binary(Key), list_to_binary(Value)} | Acc]); to_json_terms([{Key, Value} | Rest], Acc) -> to_json_terms(Rest, [{list_to_binary(Key), list_to_binary(Value)} | Acc]). diff --git a/src/chttpd/src/chttpd_misc.erl b/src/chttpd/src/chttpd_misc.erl index d0bf363f31..c22a27b982 100644 --- a/src/chttpd/src/chttpd_misc.erl +++ b/src/chttpd/src/chttpd_misc.erl @@ -252,7 +252,7 @@ replicate({Props} = PostBody, Ctx) -> ]), case rpc:call(Node, couch_replicator, replicate, [PostBody, Ctx]) of {badrpc, Reason} -> - erlang:error(Reason); + error(Reason); Res -> Res end @@ -281,7 +281,7 @@ cancel_replication(PostBody, Ctx) -> choose_node(Key) when is_binary(Key) -> Checksum = erlang:crc32(Key), - Nodes = lists:sort([node() | erlang:nodes()]), + Nodes = lists:sort([node() | nodes()]), lists:nth(1 + Checksum rem length(Nodes), Nodes); choose_node(Key) -> choose_node(?term_to_bin(Key)). diff --git a/src/chttpd/src/chttpd_node.erl b/src/chttpd/src/chttpd_node.erl index 2a67da9e36..b0ea3686cb 100644 --- a/src/chttpd/src/chttpd_node.erl +++ b/src/chttpd/src/chttpd_node.erl @@ -191,7 +191,7 @@ handle_node_req(#httpd{ "max_http_request_size", 4294967296 ), NewOpts = [{body, MochiReq0:recv_body(MaxSize)} | MochiReq0:get(opts)], - Ref = erlang:make_ref(), + Ref = make_ref(), MochiReq = mochiweb_request:new( {remote, self(), Ref}, NewOpts, @@ -286,6 +286,7 @@ get_stats() -> {run_queue, SQ}, {run_queue_dirty_cpu, DCQ}, {ets_table_count, length(ets:all())}, + {bt_engine_cache, couch_bt_engine_cache:info()}, {context_switches, element(1, statistics(context_switches))}, {reductions, element(1, statistics(reductions))}, {garbage_collection_count, NumberOfGCs}, diff --git a/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl b/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl index 3085079ad9..554da524ea 100644 --- a/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl +++ b/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl @@ -64,7 +64,7 @@ base_url() -> "http://" ++ Addr ++ ":" ++ Port. make_auth_session_string(HashAlgorithm, User, Secret, TimeStamp) -> - SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16), + SessionData = User ++ ":" ++ integer_to_list(TimeStamp, 16), Hash = couch_util:hmac(HashAlgorithm, Secret, SessionData), "AuthSession=" ++ couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)). diff --git a/src/chttpd/test/eunit/chttpd_purge_tests.erl b/src/chttpd/test/eunit/chttpd_purge_tests.erl index ef8af69717..3b91571eee 100644 --- a/src/chttpd/test/eunit/chttpd_purge_tests.erl +++ b/src/chttpd/test/eunit/chttpd_purge_tests.erl @@ -49,7 +49,6 @@ purge_test_() -> ?TDEF_FE(t_purge_only_post_allowed), ?TDEF_FE(t_empty_purge_request), ?TDEF_FE(t_ok_purge_request), - ?TDEF_FE(t_ok_purge_with_max_document_id_number), ?TDEF_FE(t_accepted_purge_request), ?TDEF_FE(t_partial_purge_request), ?TDEF_FE(t_mixed_purge_request), @@ -85,23 +84,6 @@ t_ok_purge_request(DbUrl) -> ?assertMatch(#{<<"purge_seq">> := null, <<"purged">> := IdsRevs}, Response2), ?assert(Status =:= 201 orelse Status =:= 202). -t_ok_purge_with_max_document_id_number(DbUrl) -> - PurgedDocsNum = 101, - {201, Response1} = create_docs(DbUrl, docs(PurgedDocsNum)), - IdsRevs = ids_revs(Response1), - - {400, #{<<"reason">> := Error}} = req(post, url(DbUrl, "_purge"), IdsRevs), - ?assertEqual(<<"Exceeded maximum number of documents.">>, Error), - - ok = config:set("purge", "max_document_id_number", "101", _Persist = false), - try - {Status, Response2} = req(post, url(DbUrl, "_purge"), IdsRevs), - ?assertMatch(#{<<"purge_seq">> := null, <<"purged">> := IdsRevs}, Response2), - ?assert(Status =:= 201 orelse Status =:= 202) - after - ok = config:delete("purge", "max_document_id_number", _Persist) - end. - t_accepted_purge_request(DbUrl) -> try meck:new(fabric, [passthrough]), @@ -173,24 +155,14 @@ t_over_many_ids_or_revs_purge_request(DbUrl) -> <<"doc3">> => [Rev3] }, - % Ids larger than expected - config:set("purge", "max_document_id_number", "1", _Persist = false), - try - {Status1, #{<<"reason">> := Error1}} = req(post, url(DbUrl, "_purge"), IdsRevs), - ?assertEqual(<<"Exceeded maximum number of documents.">>, Error1), - ?assertEqual(400, Status1) - after - config:delete("purge", "max_document_id_number", _Persist) - end, - % Revs larger than expected - config:set("purge", "max_revisions_number", "1", _Persist), + config:set("purge", "max_revisions_number", "1", false), try {Status2, #{<<"reason">> := Error2}} = req(post, url(DbUrl, "_purge"), IdsRevs), ?assertEqual(<<"Exceeded maximum number of revisions.">>, Error2), ?assertEqual(400, Status2) after - config:delete("purge", "max_revisions_number", _Persist) + config:delete("purge", "max_revisions_number", false) end. t_purged_infos_limit_only_get_put_allowed(DbUrl) -> @@ -316,7 +288,7 @@ create_docs(Url, Docs) -> docs(Counter) -> lists:foldl( fun(I, Acc) -> - Id = ?l2b(integer_to_list(I)), + Id = integer_to_binary(I), Doc = #{<<"_id">> => Id, <<"val">> => I}, [Doc | Acc] end, diff --git a/src/config/src/config.erl b/src/config/src/config.erl index 695bc40b9d..70b8883f93 100644 --- a/src/config/src/config.erl +++ b/src/config/src/config.erl @@ -100,7 +100,7 @@ to_integer(Int) when is_integer(Int) -> to_integer(List) when is_list(List) -> list_to_integer(List); to_integer(Bin) when is_binary(Bin) -> - list_to_integer(binary_to_list(Bin)). + binary_to_integer(Bin). get_float(Section, Key, Default) when is_float(Default) -> try @@ -125,7 +125,7 @@ to_float(List) when is_list(List) -> to_float(Int) when is_integer(Int) -> list_to_float(integer_to_list(Int) ++ ".0"); to_float(Bin) when is_binary(Bin) -> - list_to_float(binary_to_list(Bin)). + binary_to_float(Bin). get_boolean(Section, Key, Default) when is_boolean(Default) -> try diff --git a/src/config/src/config_listener_mon.erl b/src/config/src/config_listener_mon.erl index b74a613062..d794e37ce1 100644 --- a/src/config/src/config_listener_mon.erl +++ b/src/config/src/config_listener_mon.erl @@ -40,7 +40,7 @@ subscribe(Module, InitSt) -> end. init({Pid, Mod, InitSt}) -> - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), case config_listener:start(Mod, {Mod, Pid}, {Pid, InitSt}) of ok -> proc_lib:init_ack({ok, self()}), diff --git a/src/config/test/config_tests.erl b/src/config/test/config_tests.erl index 1775e4f396..4e3bf60f71 100644 --- a/src/config/test/config_tests.erl +++ b/src/config/test/config_tests.erl @@ -700,15 +700,15 @@ should_remove_handler_when_pid_exits({_Apps, Pid}) -> % Monitor the config_listener_mon process {monitored_by, [Mon]} = process_info(Pid, monitored_by), - MonRef = erlang:monitor(process, Mon), + MonRef = monitor(process, Mon), % Kill the process synchronously - PidRef = erlang:monitor(process, Pid), + PidRef = monitor(process, Pid), exit(Pid, kill), receive {'DOWN', PidRef, _, _, _} -> ok after ?TIMEOUT -> - erlang:error({timeout, config_listener_death}) + error({timeout, config_listener_death}) end, % Wait for the config_listener_mon process to @@ -716,7 +716,7 @@ should_remove_handler_when_pid_exits({_Apps, Pid}) -> receive {'DOWN', MonRef, _, _, normal} -> ok after ?TIMEOUT -> - erlang:error({timeout, config_listener_mon_death}) + error({timeout, config_listener_mon_death}) end, ?assertEqual(0, n_handlers()) @@ -728,7 +728,7 @@ should_stop_monitor_on_error({_Apps, Pid}) -> % Monitor the config_listener_mon process {monitored_by, [Mon]} = process_info(Pid, monitored_by), - MonRef = erlang:monitor(process, Mon), + MonRef = monitor(process, Mon), % Have the process throw an error ?assertEqual(ok, config:set("throw_error", "foo", "bar", false)), @@ -742,7 +742,7 @@ should_stop_monitor_on_error({_Apps, Pid}) -> receive {'DOWN', MonRef, _, _, shutdown} -> ok after ?TIMEOUT -> - erlang:error({timeout, config_listener_mon_shutdown}) + error({timeout, config_listener_mon_shutdown}) end, ?assertEqual(0, n_handlers()) @@ -784,7 +784,7 @@ should_unsubscribe_when_subscriber_gone(_Subscription, {_Apps, Pid}) -> ?assert(is_process_alive(Pid)), % Monitor subscriber process - MonRef = erlang:monitor(process, Pid), + MonRef = monitor(process, Pid), exit(Pid, kill), @@ -792,7 +792,7 @@ should_unsubscribe_when_subscriber_gone(_Subscription, {_Apps, Pid}) -> receive {'DOWN', MonRef, _, _, _} -> ok after ?TIMEOUT -> - erlang:error({timeout, config_notifier_shutdown}) + error({timeout, config_notifier_shutdown}) end, ?assertNot(is_process_alive(Pid)), @@ -1015,7 +1015,7 @@ wait_config_get(Sec, Key, Val) -> spawn_config_listener() -> Self = self(), - Pid = erlang:spawn(fun() -> + Pid = spawn(fun() -> ok = config:listen_for_changes(?MODULE, {self(), undefined}), Self ! registered, loop(undefined) @@ -1023,13 +1023,13 @@ spawn_config_listener() -> receive registered -> ok after ?TIMEOUT -> - erlang:error({timeout, config_handler_register}) + error({timeout, config_handler_register}) end, Pid. spawn_config_notifier(Subscription) -> Self = self(), - Pid = erlang:spawn(fun() -> + Pid = spawn(fun() -> ok = config:subscribe_for_changes(Subscription), Self ! registered, loop(undefined) @@ -1037,7 +1037,7 @@ spawn_config_notifier(Subscription) -> receive registered -> ok after ?TIMEOUT -> - erlang:error({timeout, config_handler_register}) + error({timeout, config_handler_register}) end, Pid. @@ -1050,7 +1050,7 @@ loop(undefined) -> {get_msg, _, _} = Msg -> loop(Msg); Msg -> - erlang:error({invalid_message, Msg}) + error({invalid_message, Msg}) end; loop({get_msg, From, Ref}) -> receive @@ -1059,7 +1059,7 @@ loop({get_msg, From, Ref}) -> {config_change, _, _, _, _} = Msg -> From ! {Ref, Msg}; Msg -> - erlang:error({invalid_message, Msg}) + error({invalid_message, Msg}) end, loop(undefined); loop({config_msg, _} = Msg) -> @@ -1067,17 +1067,17 @@ loop({config_msg, _} = Msg) -> {get_msg, From, Ref} -> From ! {Ref, Msg}; Msg -> - erlang:error({invalid_message, Msg}) + error({invalid_message, Msg}) end, loop(undefined). getmsg(Pid) -> - Ref = erlang:make_ref(), + Ref = make_ref(), Pid ! {get_msg, self(), Ref}, receive {Ref, {config_msg, Msg}} -> Msg after ?TIMEOUT -> - erlang:error({timeout, config_msg}) + error({timeout, config_msg}) end. n_handlers() -> @@ -1112,7 +1112,7 @@ wait_process_restart(Name, Timeout, Delay, Started, _Prev) -> end. stop_sync(Pid, Timeout) when is_pid(Pid) -> - MRef = erlang:monitor(process, Pid), + MRef = monitor(process, Pid), try begin catch unlink(Pid), @@ -1125,7 +1125,7 @@ stop_sync(Pid, Timeout) when is_pid(Pid) -> end end after - erlang:demonitor(MRef, [flush]) + demonitor(MRef, [flush]) end; stop_sync(_, _) -> error(badarg). diff --git a/src/couch/include/couch_db.hrl b/src/couch/include/couch_db.hrl index 9c1df21b69..138c202d57 100644 --- a/src/couch/include/couch_db.hrl +++ b/src/couch/include/couch_db.hrl @@ -182,16 +182,6 @@ db_open_options = [] }). --record(btree, { - fd, - root, - extract_kv, - assemble_kv, - less, - reduce = nil, - compression = ?DEFAULT_COMPRESSION -}). - -record(proc, { pid, lang, diff --git a/src/couch/include/couch_eunit.hrl b/src/couch/include/couch_eunit.hrl index 3ffa0506fe..2c10e257de 100644 --- a/src/couch/include/couch_eunit.hrl +++ b/src/couch/include/couch_eunit.hrl @@ -68,7 +68,7 @@ ((fun (__X) -> case (Expr) of __V when __V == __X -> ok; - __Y -> erlang:error({assertEquiv_failed, + __Y -> error({assertEquiv_failed, [{module, ?MODULE}, {line, ?LINE}, {expression, (??Expr)}, diff --git a/src/couch/include/couch_eunit_proper.hrl b/src/couch/include/couch_eunit_proper.hrl index dcf07701aa..75172bfb8e 100644 --- a/src/couch/include/couch_eunit_proper.hrl +++ b/src/couch/include/couch_eunit_proper.hrl @@ -18,12 +18,12 @@ { atom_to_list(F), {timeout, QuickcheckTimeout, - ?_assert(proper:quickcheck(?MODULE:F(), [ + ?_test(?assertMatch(true, proper:quickcheck(?MODULE:F(), [ {to_file, user}, {start_size, 2}, {numtests, NumTests}, long_result - ]))} + ]), "Failed property test '" ++ atom_to_list(F) ++ "'"))} } || {F, 0} <- ?MODULE:module_info(exports), F > 'prop_', F < 'prop`' ]). diff --git a/src/couch/priv/couch_cfile/couch_cfile.c b/src/couch/priv/couch_cfile/couch_cfile.c index 06328b0697..c7e6f16f02 100644 --- a/src/couch/priv/couch_cfile/couch_cfile.c +++ b/src/couch/priv/couch_cfile/couch_cfile.c @@ -11,7 +11,15 @@ // the License. -#ifndef _WIN32 +// Not supported on Windows or 32 bit architectures. When not supported the NIF +// will still build, but the API functions will return {error, einval}, and +// we'd fallback to the regular file API then +// +#if !defined(_WIN32) && defined(__LP64__) +#define COUCH_CFILE_SUPPORTED 1 +#endif + +#ifdef COUCH_CFILE_SUPPORTED #include #include @@ -225,7 +233,7 @@ int efile_datasync(int fd, posix_errno_t* res_errno) { // static ERL_NIF_TERM dup_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED int fd, newfd; handle_t* hdl; ErlNifRWLock *lock; @@ -293,7 +301,7 @@ static ERL_NIF_TERM dup_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) static ERL_NIF_TERM close_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; if (argc != 1 || !get_handle(env, argv[0], &hdl)) { @@ -326,7 +334,7 @@ static ERL_NIF_TERM close_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[ // static ERL_NIF_TERM close_fd_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED int fd; if (argc != 1 || !enif_is_number(env, argv[0])) { @@ -351,7 +359,7 @@ static ERL_NIF_TERM close_fd_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar // static ERL_NIF_TERM pread_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; long offset, block_size, bytes_read; SysIOVec io_vec[1]; @@ -413,7 +421,7 @@ static ERL_NIF_TERM pread_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[ // Follows implementation from prim_file_nif.c // static ERL_NIF_TERM write_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; ErlNifIOVec vec, *input = &vec; posix_errno_t res_errno = 0; @@ -452,7 +460,7 @@ static ERL_NIF_TERM write_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[ } static ERL_NIF_TERM seek_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; long result, offset; int whence; @@ -502,7 +510,7 @@ static ERL_NIF_TERM seek_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[] } static ERL_NIF_TERM datasync_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; posix_errno_t res_errno = 0; @@ -530,7 +538,7 @@ static ERL_NIF_TERM datasync_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar } static ERL_NIF_TERM truncate_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; off_t offset; @@ -568,7 +576,7 @@ static ERL_NIF_TERM truncate_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar // static ERL_NIF_TERM info_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; int fd, old_fd; @@ -601,7 +609,7 @@ static ERL_NIF_TERM info_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[] // static ERL_NIF_TERM eof_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { -#ifndef _WIN32 +#ifdef COUCH_CFILE_SUPPORTED handle_t* hdl; struct stat data; diff --git a/src/couch/priv/stats_descriptions.cfg b/src/couch/priv/stats_descriptions.cfg index 6a7120f87e..18f32a47a8 100644 --- a/src/couch/priv/stats_descriptions.cfg +++ b/src/couch/priv/stats_descriptions.cfg @@ -474,3 +474,15 @@ {type, counter}, {desc, <<"number of mango selector evaluations">>} ]}. +{[couchdb, bt_engine_cache, hits], [ + {type, counter}, + {desc, <<"number of bt_engine cache hits">>} +]}. +{[couchdb, bt_engine_cache, misses], [ + {type, counter}, + {desc, <<"number of bt_engine cache misses">>} +]}. +{[couchdb, bt_engine_cache, full], [ + {type, counter}, + {desc, <<"number of times bt_engine cache was full">>} +]}. diff --git a/src/couch/rebar.config.script b/src/couch/rebar.config.script index c4e0b7a7ee..1c9809902c 100644 --- a/src/couch/rebar.config.script +++ b/src/couch/rebar.config.script @@ -36,16 +36,16 @@ end. CouchJSPath = filename:join(["priv", CouchJSName]). Version = case os:getenv("COUCHDB_VERSION") of false -> - string:strip(os:cmd("git describe --always"), right, $\n); + string:trim(os:cmd("git describe --always")); Version0 -> - string:strip(Version0, right) + string:trim(Version0) end. GitSha = case os:getenv("COUCHDB_GIT_SHA") of false -> ""; % release builds won\'t get a fallback GitSha0 -> - string:strip(GitSha0, right) + string:trim(GitSha0) end. CouchConfig = case filelib:is_file(os:getenv("COUCHDB_CONFIG")) of @@ -151,6 +151,25 @@ ProperConfig = case code:lib_dir(proper) of _ -> [{d, 'WITH_PROPER'}] end. +DpkgArchitectureCmd = "dpkg-architecture -q DEB_HOST_MULTIARCH". +GenericMozJSIncludePaths = "-I/usr/include/mozjs-" ++ SMVsn ++ " -I/usr/local/include/mozjs-" ++ SMVsn. +GenericMozJSLibPaths = "-L/usr/local/lib". + +WithDpkgArchitecture = os:find_executable("dpkg-architecture") /= false. +WithHomebrew = os:find_executable("brew") /= false. + +MozJSIncludePath = case {WithDpkgArchitecture, WithHomebrew} of + {false, false} -> GenericMozJSIncludePaths; + {true, false} -> GenericMozJSIncludePaths ++ " -I/usr/include/" ++ string:trim(os:cmd(DpkgArchitectureCmd)) ++ "/mozjs-" ++ SMVsn; + {false, true} -> GenericMozJSIncludePaths ++ " -I/opt/homebrew/include/mozjs-" ++ SMVsn +end. + +MozJSLibPath = case {WithDpkgArchitecture, WithHomebrew} of + {false, false} -> GenericMozJSLibPaths; + {true, false} -> GenericMozJSLibPaths ++ " -L/usr/lib/" ++ string:trim(os:cmd(DpkgArchitectureCmd)); + {false, true} -> GenericMozJSLibPaths ++ " -L/opt/homebrew/lib" +end. + % The include directories (parameters for the `-I` C compiler flag) are % considered in the `configure` script as a pre-check for their existence. % Please keep them in sync. @@ -195,45 +214,20 @@ end. "-DXP_UNIX -I/usr/include/mozjs-86 -I/usr/local/include/mozjs-86 -I/opt/homebrew/include/mozjs-86/ -std=c++17 -Wno-invalid-offsetof", "-L/usr/local/lib -L /opt/homebrew/lib/ -std=c++17 -lmozjs-86 -lm" }; - {unix, _} when SMVsn == "91" -> - { - "$CFLAGS -DXP_UNIX -I/usr/include/mozjs-91 -I/usr/local/include/mozjs-91 -I/opt/homebrew/include/mozjs-91/ -std=c++17 -Wno-invalid-offsetof", - "$LDFLAGS -L/usr/local/lib -L /opt/homebrew/lib/ -std=c++17 -lmozjs-91 -lm" - }; - {unix, _} when SMVsn == "102" -> + {unix, _} when SMVsn == "91"; SMVsn == "102"; SMVsn == "115"; SMVsn == "128" -> { - "$CFLAGS -DXP_UNIX -I/usr/include/mozjs-102 -I/usr/local/include/mozjs-102 -I/opt/homebrew/include/mozjs-102/ -std=c++17 -Wno-invalid-offsetof", - "$LDFLAGS -L/usr/local/lib -L /opt/homebrew/lib/ -std=c++17 -lmozjs-102 -lm" - }; - {unix, _} when SMVsn == "115" -> - { - "$CFLAGS -DXP_UNIX -I/usr/include/mozjs-115 -I/usr/local/include/mozjs-115 -I/opt/homebrew/include/mozjs-115/ -std=c++17 -Wno-invalid-offsetof", - "$LDFLAGS -L/usr/local/lib -L /opt/homebrew/lib/ -std=c++17 -lmozjs-115 -lm" - }; - {unix, _} when SMVsn == "128" -> - { - "$CFLAGS -DXP_UNIX -I/usr/include/mozjs-128 -I/usr/local/include/mozjs-128 -I/opt/homebrew/include/mozjs-128/ -std=c++17 -Wno-invalid-offsetof", - "$LDFLAGS -L/usr/local/lib -L /opt/homebrew/lib/ -std=c++17 -lmozjs-128 -lm" + "$CFLAGS -DXP_UNIX " ++ MozJSIncludePath ++ " -std=c++17 -Wno-invalid-offsetof", + "$LDFLAGS " ++ MozJSLibPath ++ " -std=c++17 -lm -lmozjs-" ++ SMVsn }; {win32, _} when SMVsn == "91" -> { "/std:c++17 /DXP_WIN", "$LDFLAGS mozjs-91.lib" }; - {win32, _} when SMVsn == "102" -> - { - "/std:c++17 /DXP_WIN /Zc:preprocessor /utf-8", - "$LDFLAGS mozjs-102.lib" - }; - {win32, _} when SMVsn == "115" -> - { - "/std:c++17 /DXP_WIN /Zc:preprocessor /utf-8", - "$LDFLAGS mozjs-115.lib" - }; - {win32, _} when SMVsn == "128" -> + {win32, _} when SMVsn == "102"; SMVsn == "115"; SMVsn == "128" -> { "/std:c++17 /DXP_WIN /Zc:preprocessor /utf-8", - "$LDFLAGS mozjs-128.lib" + "$LDFLAGS mozjs-" ++ SMVsn ++ ".lib" } end. @@ -273,12 +267,12 @@ end. IcuIncludePath = case WithBrew of false -> GenericIcuIncludePaths; - true -> "-I" ++ string:strip(os:cmd(BrewIcuPrefixCmd), right, $\n) ++ "/include" + true -> "-I" ++ string:trim(os:cmd(BrewIcuPrefixCmd)) ++ "/include" end. IcuLibPath = case WithBrew of false -> GenericIcuLibPaths; - true -> "-L" ++ string:strip(os:cmd(BrewIcuPrefixCmd), right, $\n) ++ "/lib" + true -> "-L" ++ string:trim(os:cmd(BrewIcuPrefixCmd)) ++ "/lib" end. IcuEnv = [{"DRV_CFLAGS", "$DRV_CFLAGS -DPIC -O2 -fno-common"}, diff --git a/src/couch/src/couch_att.erl b/src/couch/src/couch_att.erl index 5ca3927e70..c7e051a285 100644 --- a/src/couch/src/couch_att.erl +++ b/src/couch/src/couch_att.erl @@ -491,7 +491,7 @@ encoded_lengths_from_json(Props) -> EncodedLen = Len; EncodingValue -> EncodedLen = couch_util:get_value(<<"encoded_length">>, Props, Len), - Encoding = list_to_existing_atom(binary_to_list(EncodingValue)) + Encoding = binary_to_existing_atom(EncodingValue) end, {Len, EncodedLen, Encoding}. @@ -585,7 +585,7 @@ flush_data(Db, Fun, Att) when is_function(Fun) -> end) end; flush_data(Db, {follows, Parser, Ref}, Att) -> - ParserRef = erlang:monitor(process, Parser), + ParserRef = monitor(process, Parser), Fun = fun() -> Parser ! {get_bytes, Ref, self()}, receive @@ -600,7 +600,7 @@ flush_data(Db, {follows, Parser, Ref}, Att) -> try flush_data(Db, Fun, store(data, Fun, Att)) after - erlang:demonitor(ParserRef, [flush]) + demonitor(ParserRef, [flush]) end; flush_data(Db, {stream, StreamEngine}, Att) -> case couch_db:is_active_stream(Db, StreamEngine) of @@ -645,7 +645,7 @@ foldl(DataFun, Att, Fun, Acc) when is_function(DataFun) -> Len = fetch(att_len, Att), fold_streamed_data(DataFun, Len, Fun, Acc); foldl({follows, Parser, Ref}, Att, Fun, Acc) -> - ParserRef = erlang:monitor(process, Parser), + ParserRef = monitor(process, Parser), DataFun = fun() -> Parser ! {get_bytes, Ref, self()}, receive @@ -660,7 +660,7 @@ foldl({follows, Parser, Ref}, Att, Fun, Acc) -> try foldl(DataFun, store(data, DataFun, Att), Fun, Acc) after - erlang:demonitor(ParserRef, [flush]) + demonitor(ParserRef, [flush]) end. range_foldl(Att, From, To, Fun, Acc) -> @@ -781,7 +781,7 @@ open_stream(StreamSrc, Data) -> true -> StreamSrc(Data); false -> - erlang:error({invalid_stream_source, StreamSrc}) + error({invalid_stream_source, StreamSrc}) end end. diff --git a/src/couch/src/couch_base32.erl b/src/couch/src/couch_base32.erl index 776fe773dd..03267ced07 100644 --- a/src/couch/src/couch_base32.erl +++ b/src/couch/src/couch_base32.erl @@ -150,7 +150,7 @@ decode(Encoded, ByteOffset, Acc) -> find_in_set(Char) -> case binary:match(?SET, Char) of nomatch -> - erlang:error(not_base32); + error(not_base32); {Offset, _} -> Offset end. diff --git a/src/couch/src/couch_bt_engine.erl b/src/couch/src/couch_bt_engine.erl index ad84e0db85..8f99926ca4 100644 --- a/src/couch/src/couch_bt_engine.erl +++ b/src/couch/src/couch_bt_engine.erl @@ -114,6 +114,19 @@ -include_lib("couch/include/couch_db.hrl"). -include("couch_bt_engine.hrl"). +% Commonly used header fields (used more than once in this module) +-define(UPDATE_SEQ, update_seq). +-define(SECURITY_PTR, security_ptr). +-define(PROPS_PTR, props_ptr). +-define(REVS_LIMIT, revs_limit). +-define(PURGE_INFOS_LIMIT, purge_infos_limit). +-define(COMPACTED_SEQ, compacted_seq). + +-define(DEFAULT_BTREE_CACHE_DEPTH, 3). +% Priority is about how long the entry will survive in the cache initially. A +% period is about 2 seconds and each period the value is halved. +-define(HEADER_CACHE_PRIORITY, 16). + exists(FilePath) -> case is_file(FilePath) of true -> @@ -196,14 +209,14 @@ handle_db_updater_info({'DOWN', Ref, _, _, _}, #st{fd_monitor = Ref} = St) -> {stop, normal, St#st{fd = undefined, fd_monitor = closed}}. incref(St) -> - {ok, St#st{fd_monitor = erlang:monitor(process, St#st.fd)}}. + {ok, St#st{fd_monitor = monitor(process, St#st.fd)}}. decref(St) -> - true = erlang:demonitor(St#st.fd_monitor, [flush]), + true = demonitor(St#st.fd_monitor, [flush]), ok. monitored_by(St) -> - case erlang:process_info(St#st.fd, monitored_by) of + case process_info(St#st.fd, monitored_by) of {monitored_by, Pids} -> lists:filter(fun is_pid/1, Pids); _ -> @@ -211,7 +224,7 @@ monitored_by(St) -> end. get_compacted_seq(#st{header = Header}) -> - couch_bt_engine_header:get(Header, compacted_seq). + couch_bt_engine_header:get(Header, ?COMPACTED_SEQ). get_del_doc_count(#st{} = St) -> {ok, Reds} = couch_btree:full_reduce(St#st.id_tree), @@ -242,10 +255,10 @@ get_oldest_purge_seq(#st{purge_seq_tree = PurgeSeqTree}) -> PurgeSeq. get_purge_infos_limit(#st{header = Header}) -> - couch_bt_engine_header:get(Header, purge_infos_limit). + couch_bt_engine_header:get(Header, ?PURGE_INFOS_LIMIT). get_revs_limit(#st{header = Header}) -> - couch_bt_engine_header:get(Header, revs_limit). + couch_bt_engine_header:get(Header, ?REVS_LIMIT). get_size_info(#st{} = St) -> {ok, FileSize} = couch_file:bytes(St#st.fd), @@ -305,26 +318,14 @@ get_partition_info(#st{} = St, Partition) -> ]} ]. -get_security(#st{header = Header} = St) -> - case couch_bt_engine_header:get(Header, security_ptr) of - undefined -> - []; - Pointer -> - {ok, SecProps} = couch_file:pread_term(St#st.fd, Pointer), - SecProps - end. +get_security(#st{} = St) -> + get_header_term(St, ?SECURITY_PTR, []). -get_props(#st{header = Header} = St) -> - case couch_bt_engine_header:get(Header, props_ptr) of - undefined -> - []; - Pointer -> - {ok, Props} = couch_file:pread_term(St#st.fd, Pointer), - Props - end. +get_props(#st{} = St) -> + get_header_term(St, ?PROPS_PTR, []). get_update_seq(#st{header = Header}) -> - couch_bt_engine_header:get(Header, update_seq). + couch_bt_engine_header:get(Header, ?UPDATE_SEQ). get_uuid(#st{header = Header}) -> couch_bt_engine_header:get(Header, uuid). @@ -332,7 +333,7 @@ get_uuid(#st{header = Header}) -> set_revs_limit(#st{header = Header} = St, RevsLimit) -> NewSt = St#st{ header = couch_bt_engine_header:set(Header, [ - {revs_limit, RevsLimit} + {?REVS_LIMIT, RevsLimit} ]), needs_commit = true }, @@ -341,33 +342,17 @@ set_revs_limit(#st{header = Header} = St, RevsLimit) -> set_purge_infos_limit(#st{header = Header} = St, PurgeInfosLimit) -> NewSt = St#st{ header = couch_bt_engine_header:set(Header, [ - {purge_infos_limit, PurgeInfosLimit} + {?PURGE_INFOS_LIMIT, PurgeInfosLimit} ]), needs_commit = true }, {ok, increment_update_seq(NewSt)}. -set_security(#st{header = Header} = St, NewSecurity) -> - Options = [{compression, St#st.compression}], - {ok, Ptr, _} = couch_file:append_term(St#st.fd, NewSecurity, Options), - NewSt = St#st{ - header = couch_bt_engine_header:set(Header, [ - {security_ptr, Ptr} - ]), - needs_commit = true - }, - {ok, increment_update_seq(NewSt)}. +set_security(#st{} = St, NewSecurity) -> + {ok, increment_update_seq(set_header_term(St, ?SECURITY_PTR, NewSecurity))}. -set_props(#st{header = Header} = St, Props) -> - Options = [{compression, St#st.compression}], - {ok, Ptr, _} = couch_file:append_term(St#st.fd, Props, Options), - NewSt = St#st{ - header = couch_bt_engine_header:set(Header, [ - {props_ptr, Ptr} - ]), - needs_commit = true - }, - {ok, increment_update_seq(NewSt)}. +set_props(#st{} = St, Props) -> + {ok, increment_update_seq(set_header_term(St, ?PROPS_PTR, Props))}. open_docs(#st{} = St, DocIds) -> Results = couch_btree:lookup(St#st.id_tree, DocIds), @@ -480,14 +465,14 @@ write_doc_infos(#st{} = St, Pairs, LocalDocs) -> NewUpdateSeq = lists:foldl( fun(#full_doc_info{update_seq = Seq}, Acc) -> - erlang:max(Seq, Acc) + max(Seq, Acc) end, get_update_seq(St), Add ), NewHeader = couch_bt_engine_header:set(St#st.header, [ - {update_seq, NewUpdateSeq} + {?UPDATE_SEQ, NewUpdateSeq} ]), {ok, St#st{ @@ -509,7 +494,7 @@ purge_docs(#st{} = St, Pairs, PurgeInfos) -> RemDocIds = [Old#full_doc_info.id || {Old, not_found} <- Pairs], RemSeqs = [Old#full_doc_info.update_seq || {Old, _} <- Pairs], DocsToAdd = [New || {_, New} <- Pairs, New /= not_found], - CurrSeq = couch_bt_engine_header:get(St#st.header, update_seq), + CurrSeq = couch_bt_engine_header:get(St#st.header, ?UPDATE_SEQ), Seqs = [FDI#full_doc_info.update_seq || FDI <- DocsToAdd], NewSeq = lists:max([CurrSeq | Seqs]), @@ -522,7 +507,7 @@ purge_docs(#st{} = St, Pairs, PurgeInfos) -> false -> NewSeq end, Header = couch_bt_engine_header:set(St#st.header, [ - {update_seq, UpdateSeq} + {?UPDATE_SEQ, UpdateSeq} ]), {ok, IdTree2} = couch_btree:add_remove(IdTree, DocsToAdd, RemDocIds), @@ -562,9 +547,7 @@ commit_data(St) -> case NewHeader /= OldHeader orelse NeedsCommit of true -> - couch_file:sync(Fd), - ok = couch_file:write_header(Fd, NewHeader), - couch_file:sync(Fd), + ok = couch_file:write_header(Fd, NewHeader, [sync]), couch_stats:increment_counter([couchdb, commits]), {ok, St#st{ header = NewHeader, @@ -615,7 +598,7 @@ fold_purge_infos(St, StartSeq0, UserFun, UserAcc, Options) -> % automatically. if MinSeq =< StartSeq -> ok; - true -> erlang:error({invalid_start_purge_seq, StartSeq0, MinSeq}) + true -> error({invalid_start_purge_seq, StartSeq0, MinSeq}) end, Wrapper = fun(Info, _Reds, UAcc) -> UserFun(Info, UAcc) @@ -804,30 +787,16 @@ purge_tree_reduce(rereduce, Reds) -> set_update_seq(#st{header = Header} = St, UpdateSeq) -> {ok, St#st{ header = couch_bt_engine_header:set(Header, [ - {update_seq, UpdateSeq} + {?UPDATE_SEQ, UpdateSeq} ]), needs_commit = true }}. -copy_security(#st{header = Header} = St, SecProps) -> - Options = [{compression, St#st.compression}], - {ok, Ptr, _} = couch_file:append_term(St#st.fd, SecProps, Options), - {ok, St#st{ - header = couch_bt_engine_header:set(Header, [ - {security_ptr, Ptr} - ]), - needs_commit = true - }}. +copy_security(#st{} = St, SecProps) -> + {ok, set_header_term(St, ?SECURITY_PTR, SecProps)}. -copy_props(#st{header = Header} = St, Props) -> - Options = [{compression, St#st.compression}], - {ok, Ptr, _} = couch_file:append_term(St#st.fd, Props, Options), - {ok, St#st{ - header = couch_bt_engine_header:set(Header, [ - {props_ptr, Ptr} - ]), - needs_commit = true - }}. +copy_props(#st{} = St, Props) -> + {ok, set_header_term(St, ?PROPS_PTR, Props)}. open_db_file(FilePath, Options) -> case couch_file:open(FilePath, Options) of @@ -864,7 +833,8 @@ init_state(FilePath, Fd, Header0, Options) -> {split, fun ?MODULE:id_tree_split/1}, {join, fun ?MODULE:id_tree_join/2}, {reduce, fun ?MODULE:id_tree_reduce/2}, - {compression, Compression} + {compression, Compression}, + {cache_depth, btree_cache_depth()} ]), SeqTreeState = couch_bt_engine_header:seq_tree_state(Header), @@ -872,28 +842,32 @@ init_state(FilePath, Fd, Header0, Options) -> {split, fun ?MODULE:seq_tree_split/1}, {join, fun ?MODULE:seq_tree_join/2}, {reduce, fun ?MODULE:seq_tree_reduce/2}, - {compression, Compression} + {compression, Compression}, + {cache_depth, btree_cache_depth()} ]), LocalTreeState = couch_bt_engine_header:local_tree_state(Header), {ok, LocalTree} = couch_btree:open(LocalTreeState, Fd, [ {split, fun ?MODULE:local_tree_split/1}, {join, fun ?MODULE:local_tree_join/2}, - {compression, Compression} + {compression, Compression}, + {cache_depth, btree_cache_depth()} ]), PurgeTreeState = couch_bt_engine_header:purge_tree_state(Header), {ok, PurgeTree} = couch_btree:open(PurgeTreeState, Fd, [ {split, fun ?MODULE:purge_tree_split/1}, {join, fun ?MODULE:purge_tree_join/2}, - {reduce, fun ?MODULE:purge_tree_reduce/2} + {reduce, fun ?MODULE:purge_tree_reduce/2}, + {cache_depth, btree_cache_depth()} ]), PurgeSeqTreeState = couch_bt_engine_header:purge_seq_tree_state(Header), {ok, PurgeSeqTree} = couch_btree:open(PurgeSeqTreeState, Fd, [ {split, fun ?MODULE:purge_seq_tree_split/1}, {join, fun ?MODULE:purge_seq_tree_join/2}, - {reduce, fun ?MODULE:purge_tree_reduce/2} + {reduce, fun ?MODULE:purge_tree_reduce/2}, + {cache_depth, btree_cache_depth()} ]), ok = couch_file:set_db_pid(Fd, self()), @@ -901,7 +875,7 @@ init_state(FilePath, Fd, Header0, Options) -> St = #st{ filepath = FilePath, fd = Fd, - fd_monitor = erlang:monitor(process, Fd), + fd_monitor = monitor(process, Fd), header = Header, needs_commit = false, id_tree = IdTree, @@ -933,22 +907,20 @@ update_header(St, Header) -> ]). increment_update_seq(#st{header = Header} = St) -> - UpdateSeq = couch_bt_engine_header:get(Header, update_seq), + UpdateSeq = couch_bt_engine_header:get(Header, ?UPDATE_SEQ), St#st{ header = couch_bt_engine_header:set(Header, [ - {update_seq, UpdateSeq + 1} + {?UPDATE_SEQ, UpdateSeq + 1} ]) }. set_default_security_object(Fd, Header, Compression, Options) -> - case couch_bt_engine_header:get(Header, security_ptr) of + case couch_bt_engine_header:get(Header, ?SECURITY_PTR) of Pointer when is_integer(Pointer) -> Header; _ -> Default = couch_util:get_value(default_security_object, Options), - AppendOpts = [{compression, Compression}], - {ok, Ptr, _} = couch_file:append_term(Fd, Default, AppendOpts), - couch_bt_engine_header:set(Header, security_ptr, Ptr) + set_header_term(Fd, Header, ?SECURITY_PTR, Default, Compression) end. % This function is here, and not in couch_bt_engine_header @@ -1015,9 +987,7 @@ init_set_props(Fd, Header, Options) -> Header; InitialProps -> Compression = couch_compress:get_compression_method(), - AppendOpts = [{compression, Compression}], - {ok, Ptr, _} = couch_file:append_term(Fd, InitialProps, AppendOpts), - couch_bt_engine_header:set(Header, props_ptr, Ptr) + set_header_term(Fd, Header, ?PROPS_PTR, InitialProps, Compression) end. delete_compaction_files(FilePath) -> @@ -1190,9 +1160,9 @@ finish_compaction_int(#st{} = OldSt, #st{} = NewSt1) -> {ok, NewSt2} = commit_data(NewSt1#st{ header = couch_bt_engine_header:set(Header, [ - {compacted_seq, get_update_seq(OldSt)}, - {revs_limit, get_revs_limit(OldSt)}, - {purge_infos_limit, get_purge_infos_limit(OldSt)} + {?COMPACTED_SEQ, get_update_seq(OldSt)}, + {?REVS_LIMIT, get_revs_limit(OldSt)}, + {?PURGE_INFOS_LIMIT, get_purge_infos_limit(OldSt)} ]), local_tree = NewLocal2 }), @@ -1229,3 +1199,44 @@ is_file(Path) -> {ok, #file_info{type = directory}} -> true; _ -> false end. + +get_header_term(#st{header = Header} = St, Key, Default) when is_atom(Key) -> + case couch_bt_engine_header:get(Header, Key) of + undefined -> + Default; + Pointer when is_integer(Pointer) -> + case couch_bt_engine_cache:lookup({St#st.fd, Pointer}) of + undefined -> + {ok, Term} = couch_file:pread_term(St#st.fd, Pointer), + couch_bt_engine_cache:insert({St#st.fd, Pointer}, Term, ?HEADER_CACHE_PRIORITY), + Term; + Term -> + Term + end + end. + +set_header_term(#st{} = St, Key, Term) when is_atom(Key) -> + #st{fd = Fd, header = Header, compression = Compression} = St, + St#st{ + header = set_header_term(Fd, Header, Key, Term, Compression), + needs_commit = true + }. + +set_header_term(Fd, Header, Key, Term, Compression) when is_atom(Key) -> + case couch_bt_engine_header:get(Header, Key) of + Pointer when is_integer(Pointer) -> + % Reset old one to 0 usage. Some old snapshot may still + % see it and use. But it will only survive only one more + % interval at most otherwise + couch_bt_engine_cache:reset({Fd, Pointer}); + _ -> + ok + end, + TermOpts = [{compression, Compression}], + {ok, Ptr, _} = couch_file:append_term(Fd, Term, TermOpts), + Result = couch_bt_engine_header:set(Header, Key, Ptr), + couch_bt_engine_cache:insert({Fd, Ptr}, Term, ?HEADER_CACHE_PRIORITY), + Result. + +btree_cache_depth() -> + config:get_integer("bt_engine_cache", "db_btree_cache_depth", ?DEFAULT_BTREE_CACHE_DEPTH). diff --git a/src/couch/src/couch_bt_engine_cache.erl b/src/couch/src/couch_bt_engine_cache.erl new file mode 100644 index 0000000000..a395649577 --- /dev/null +++ b/src/couch/src/couch_bt_engine_cache.erl @@ -0,0 +1,292 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_bt_engine_cache). + +-include_lib("stdlib/include/ms_transform.hrl"). + +% Main API +% +-export([ + insert/2, + insert/3, + reset/1, + lookup/1, + info/0, + tables/0 +]). + +% Supervision and start API +% +-export([ + create_tables/0, + sup_children/0, + start_link/1, + init/1 +]). + +-define(DEFAULT_SIZE, 67108864). +-define(DEFAULT_LEAVE_PERCENT, 30). +-define(INTERVAL_MSEC, 3000). +% How often cleaners check if the ets size increases. This is used in cases +% when initially, at the start the cleaning interval, ets table size is below +% "leave percent". Then we skip cleaning all the 0 usage entries. However, if a +% new set of requests come in soon after, but before the next clean-up interval +% they'll fail since the cache would be full. To be able to react quicker and +% make room, cleaners poll table sizes a bit more often. +-define(CLEANUP_INTERVAL_MSEC, 100). +% 1 bsl 58, power of 2 that's still an immediate integer +-define(MAX_PRIORITY, 288230376151711744). +-define(PTERM_KEY, {?MODULE, caches}). +% Metrics +-define(HITS, hits). +-define(MISSES, misses). +-define(FULL, full). + +-record(cache, {tid, max_size}). + +% Main API + +insert(Key, Term) -> + insert(Key, Term, 1). + +insert(Key, Term, Priority) when is_integer(Priority) -> + Priority1 = min(?MAX_PRIORITY, max(0, Priority)), + case get_cache(Key) of + #cache{tid = Tid, max_size = Max} -> + case ets:info(Tid, memory) < Max of + true -> + case ets:insert_new(Tid, {Key, Priority1, Term}) of + true -> + true; + false -> + bump_usage(Tid, Key), + false + end; + false -> + bump_metric(?FULL), + false + end; + undefined -> + false + end. + +reset(Key) -> + case get_cache(Key) of + #cache{tid = Tid} -> reset_usage(Tid, Key); + undefined -> true + end. + +lookup(Key) -> + case get_cache(Key) of + #cache{tid = Tid} -> + case ets:lookup_element(Tid, Key, 3, undefined) of + undefined -> + bump_metric(?MISSES), + undefined; + Term -> + bump_usage(Tid, Key), + bump_metric(?HITS), + Term + end; + undefined -> + undefined + end. + +info() -> + case persistent_term:get(?PTERM_KEY, undefined) of + Caches when is_tuple(Caches) -> + SizeMem = [info(C) || C <- tuple_to_list(Caches)], + MaxMem = [Max || #cache{max_size = Max} <- tuple_to_list(Caches)], + {Sizes, Mem} = lists:unzip(SizeMem), + #{ + size => lists:sum(Sizes), + memory => lists:sum(Mem), + max_memory => lists:sum(MaxMem) * wordsize(), + full => sample_metric(?FULL), + hits => sample_metric(?HITS), + misses => sample_metric(?MISSES), + shard_count => shard_count() + }; + undefined -> + #{} + end. + +tables() -> + case persistent_term:get(?PTERM_KEY, undefined) of + Caches when is_tuple(Caches) -> + [Tid || #cache{tid = Tid} <- tuple_to_list(Caches)]; + undefined -> + [] + end. + +% Supervisor helper functions + +create_tables() -> + BtCaches = [new() || _ <- lists:seq(1, shard_count())], + persistent_term:put(?PTERM_KEY, list_to_tuple(BtCaches)). + +sup_children() -> + [sup_child(I) || I <- lists:seq(1, shard_count())]. + +sup_child(N) -> + Name = list_to_atom("couch_bt_engine_cache_" ++ integer_to_list(N)), + #{id => Name, start => {?MODULE, start_link, [N]}, shutdown => brutal_kill}. + +% Process start and main loop + +start_link(N) when is_integer(N) -> + {ok, proc_lib:spawn_link(?MODULE, init, [N])}. + +init(N) -> + Caches = persistent_term:get(?PTERM_KEY), + Cache = #cache{tid = Tid} = element(N, Caches), + ets:delete_all_objects(Tid), + loop(Cache). + +loop(#cache{tid = Tid} = Cache) -> + decay(Tid), + Next = now_msec() + wait_interval(?INTERVAL_MSEC), + clean(Cache, Next - ?CLEANUP_INTERVAL_MSEC), + remove_dead(Cache), + WaitLeft = max(10, Next - now_msec()), + timer:sleep(WaitLeft), + loop(Cache). + +% Clean unused procs. If we haven't cleaned any keep polling at a higher +% rate so we react quicker if a new set of entries are added. +% +clean(#cache{tid = Tid} = Cache, Until) -> + case now_msec() < Until of + true -> + case should_clean(Cache) of + true -> + ets:match_delete(Tid, {'_', 0, '_'}); + false -> + timer:sleep(wait_interval(?CLEANUP_INTERVAL_MSEC)), + clean(Cache, Until) + end; + false -> + false + end. + +should_clean(#cache{tid = Tid, max_size = Max}) -> + ets:info(Tid, memory) >= Max * leave_percent() / 100. + +remove_dead(#cache{tid = Tid}) -> + All = pids(Tid), + Alive = sets:filter(fun is_process_alive/1, All), + Dead = sets:subtract(All, Alive), + % In OTP 27+ use sets:map/2 + Fun = fun(Pid, _) -> ets:match_delete(Tid, {{Pid, '_'}, '_', '_'}) end, + sets:fold(Fun, true, Dead). + +pids(Tid) -> + Acc = couch_util:new_set(), + try + ets:foldl(fun pids_fold/2, Acc, Tid) + catch + error:badarg -> Acc + end. + +pids_fold({{Pid, _}, _, _}, Acc) when is_pid(Pid) -> + sets:add_element(Pid, Acc); +pids_fold({_, _, _}, Acc) -> + Acc. + +new() -> + Opts = [public, {write_concurrency, true}, {read_concurrency, true}], + Max0 = round(max_size() / wordsize() / shard_count()), + % Some per-table overhead for the table metadata + Max = Max0 + round(250 * 1024 / wordsize()), + #cache{tid = ets:new(?MODULE, Opts), max_size = Max}. + +get_cache(Term) -> + case persistent_term:get(?PTERM_KEY, undefined) of + Caches when is_tuple(Caches) -> + Index = erlang:phash2(Term, tuple_size(Caches)), + #cache{} = element(1 + Index, Caches); + undefined -> + undefined + end. + +bump_usage(Tid, Key) -> + % We're updating the second field incrementing it by 1 and clamping it + % at ?MAX_PRIORITY. We don't set the default for the update_counter + % specifically to avoid creating bogus entries just from updating the + % counter, so expect the error:badarg here sometimes. + UpdateOp = {2, 1, ?MAX_PRIORITY, ?MAX_PRIORITY}, + try + ets:update_counter(Tid, Key, UpdateOp) + catch + error:badarg -> ok + end. + +reset_usage(Tid, Key) -> + % Reset the value of the usage to 0. Since max value is ?MAX_PRIORITY, + % subtract that and clamp it at 0. Do not provide a default since if an + % entry is missing we don't want to create a bogus one from this operation. + UpdateOp = {2, -?MAX_PRIORITY, 0, 0}, + try + ets:update_counter(Tid, Key, UpdateOp) + catch + error:badarg -> ok + end. + +info(#cache{tid = Tid}) -> + Memory = ets:info(Tid, memory) * wordsize(), + Size = ets:info(Tid, size), + {Size, Memory}. + +decay(Tid) -> + MatchSpec = ets:fun2ms( + fun({Key, Usage, Term}) when Usage > 0 -> + {Key, Usage bsr 1, Term} + end + ), + ets:select_replace(Tid, MatchSpec). + +shard_count() -> + % Use a minimum size of 16 even for there are less than 16 schedulers + % to keep the total tables size a bit smaller + max(16, erlang:system_info(schedulers)). + +wait_interval(Interval) -> + Jitter = rand:uniform(max(1, Interval bsr 1)), + Interval + Jitter. + +max_size() -> + config:get_integer("bt_engine_cache", "max_size", ?DEFAULT_SIZE). + +leave_percent() -> + Val = config:get_integer("bt_engine_cache", "leave_percent", ?DEFAULT_LEAVE_PERCENT), + max(0, min(90, Val)). + +now_msec() -> + erlang:monotonic_time(millisecond). + +bump_metric(Metric) when is_atom(Metric) -> + couch_stats:increment_counter([couchdb, bt_engine_cache, Metric]). + +sample_metric(Metric) when is_atom(Metric) -> + try + couch_stats:sample([couchdb, bt_engine_cache, Metric]) + catch + throw:unknown_metric -> + 0 + end. + +% ETS sizes are expressed in "words". To get the byte size need to multiply +% memory sizes by the wordsize. On 64 bit systems this should be 8 +% +wordsize() -> + erlang:system_info(wordsize). diff --git a/src/couch/src/couch_bt_engine_compactor.erl b/src/couch/src/couch_bt_engine_compactor.erl index 8ed55b5c39..37295cb16f 100644 --- a/src/couch/src/couch_bt_engine_compactor.erl +++ b/src/couch/src/couch_bt_engine_compactor.erl @@ -147,7 +147,7 @@ open_compaction_files(DbName, OldSt, Options) -> } end, unlink(DataFd), - erlang:monitor(process, MetaFd), + monitor(process, MetaFd), {ok, CompSt}. copy_purge_info(#comp_st{} = CompSt) -> @@ -381,7 +381,7 @@ copy_compact(#comp_st{} = CompSt) -> % Copy general properties over Props = couch_bt_engine:get_props(St), - {ok, NewSt5} = couch_bt_engine:set_props(NewSt4, Props), + {ok, NewSt5} = couch_bt_engine:copy_props(NewSt4, Props), FinalUpdateSeq = couch_bt_engine:get_update_seq(St), {ok, NewSt6} = couch_bt_engine:set_update_seq(NewSt5, FinalUpdateSeq), @@ -693,7 +693,7 @@ merge_lookups([#doc_info{} = DI | RestInfos], [{ok, FDI} | RestLookups]) -> % Assert we've matched our lookups if DI#doc_info.id == FDI#full_doc_info.id -> ok; - true -> erlang:error({mismatched_doc_infos, DI#doc_info.id}) + true -> error({mismatched_doc_infos, DI#doc_info.id}) end, [FDI | merge_lookups(RestInfos, RestLookups)]; merge_lookups([FDI | RestInfos], Lookups) -> diff --git a/src/couch/src/couch_bt_engine_header.erl b/src/couch/src/couch_bt_engine_header.erl index 3581b1e398..e6e211de3c 100644 --- a/src/couch/src/couch_bt_engine_header.erl +++ b/src/couch/src/couch_bt_engine_header.erl @@ -204,20 +204,19 @@ upgrade_tuple(Old) when is_record(Old, db_header) -> Old; upgrade_tuple(Old) when is_tuple(Old) -> NewSize = record_info(size, db_header), - if - tuple_size(Old) < NewSize -> ok; - true -> erlang:error({invalid_header_size, Old}) - end, - {_, New} = lists:foldl( - fun(Val, {Idx, Hdr}) -> - {Idx + 1, setelement(Idx, Hdr, Val)} + Upgrade = tuple_size(Old) < NewSize, + ProhibitDowngrade = config:get_boolean("couchdb", "prohibit_downgrade", true), + OldKVs = + case {Upgrade, ProhibitDowngrade} of + {true, AnyBool} when is_boolean(AnyBool) -> tuple_to_list(Old); + {false, true} -> error({invalid_header_size, Old}); + {false, false} -> lists:sublist(tuple_to_list(Old), NewSize) end, - {1, #db_header{}}, - tuple_to_list(Old) - ), + FoldFun = fun(Val, {Idx, Hdr}) -> {Idx + 1, setelement(Idx, Hdr, Val)} end, + {_, New} = lists:foldl(FoldFun, {1, #db_header{}}, OldKVs), if is_record(New, db_header) -> ok; - true -> erlang:error({invalid_header_extension, {Old, New}}) + true -> error({invalid_header_extension, {Old, New}}) end, New. @@ -338,7 +337,7 @@ latest(_Else) -> undefined. -ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). +-include_lib("couch/include/couch_eunit.hrl"). mk_header(Vsn) -> { @@ -478,4 +477,61 @@ get_epochs_from_old_header_test() -> Vsn5Header = mk_header(5), ?assertEqual(undefined, epochs(Vsn5Header)). +tuple_uprade_test_() -> + { + foreach, + fun() -> + Ctx = test_util:start_couch(), + config:set("couchdb", "prohibit_downgrade", "true", false), + Ctx + end, + fun(Ctx) -> + config:delete("couchdb", "prohibit_downgrade", false), + test_util:stop_couch(Ctx) + end, + [ + ?TDEF_FE(t_upgrade_tuple_same_size), + ?TDEF_FE(t_upgrade_tuple), + ?TDEF_FE(t_downgrade_default), + ?TDEF_FE(t_downgrade_allowed) + ] + }. + +t_upgrade_tuple_same_size(_) -> + Hdr = #db_header{disk_version = ?LATEST_DISK_VERSION}, + Hdr1 = upgrade_tuple(Hdr), + ?assertEqual(Hdr, Hdr1). + +t_upgrade_tuple(_) -> + Hdr = {db_header, ?LATEST_DISK_VERSION, 101}, + Hdr1 = upgrade_tuple(Hdr), + ?assertMatch( + #db_header{ + disk_version = ?LATEST_DISK_VERSION, + update_seq = 101, + purge_infos_limit = 1000 + }, + Hdr1 + ). + +t_downgrade_default(_) -> + Junk = lists:duplicate(50, x), + Hdr = list_to_tuple([db_header, ?LATEST_DISK_VERSION] ++ Junk), + % Not allowed by default + ?assertError({invalid_header_size, _}, upgrade_tuple(Hdr)). + +t_downgrade_allowed(_) -> + Junk = lists:duplicate(50, x), + Hdr = list_to_tuple([db_header, ?LATEST_DISK_VERSION, 42] ++ Junk), + config:set("couchdb", "prohibit_downgrade", "false", false), + Hdr1 = upgrade_tuple(Hdr), + ?assert(is_record(Hdr1, db_header)), + ?assertMatch( + #db_header{ + disk_version = ?LATEST_DISK_VERSION, + update_seq = 42 + }, + Hdr1 + ). + -endif. diff --git a/src/couch/src/couch_btree.erl b/src/couch/src/couch_btree.erl index b974a22eec..1519b1fbcb 100644 --- a/src/couch/src/couch_btree.erl +++ b/src/couch/src/couch_btree.erl @@ -14,11 +14,30 @@ -export([open/2, open/3, query_modify/4, add/2, add_remove/3]). -export([fold/4, full_reduce/1, final_reduce/2, size/1, foldl/3, foldl/4]). --export([fold_reduce/4, lookup/2, get_state/1, set_options/2]). +-export([fold_reduce/4, lookup/2, set_options/2]). +-export([is_btree/1, get_state/1, get_fd/1, get_reduce_fun/1]). -export([extract/2, assemble/3, less/3]). -include_lib("couch/include/couch_db.hrl"). +-define(DEFAULT_CHUNK_SIZE, 1279). + +% For the btree cache, the priority of the root node will be +% this value. The priority is roughly how many cleanup interval +% (second) they'll survive without any updates in the cache +-define(ROOT_NODE_CACHE_PRIORITY, 8). + +-record(btree, { + fd, + root, + extract_kv, + assemble_kv, + less, + reduce = nil, + compression = ?DEFAULT_COMPRESSION, + cache_depth = 0 +}). + -define(FILL_RATIO, 0.5). extract(#btree{extract_kv = undefined}, Value) -> @@ -31,11 +50,18 @@ assemble(#btree{assemble_kv = undefined}, Key, Value) -> assemble(#btree{assemble_kv = Assemble}, Key, Value) -> Assemble(Key, Value). +-compile({inline, [less/3]}). less(#btree{less = undefined}, A, B) -> A < B; less(#btree{less = Less}, A, B) -> Less(A, B). +-compile({inline, [less_eq/3]}). +less_eq(#btree{less = undefined}, A, B) -> + A =< B; +less_eq(#btree{less = Less}, A, B) -> + Less(A, B) orelse not Less(B, A). + % pass in 'nil' for State if a new Btree. open(State, Fd) -> {ok, #btree{root = State, fd = Fd}}. @@ -51,14 +77,27 @@ set_options(Bt, [{less, Less} | Rest]) -> set_options(Bt, [{reduce, Reduce} | Rest]) -> set_options(Bt#btree{reduce = Reduce}, Rest); set_options(Bt, [{compression, Comp} | Rest]) -> - set_options(Bt#btree{compression = Comp}, Rest). + set_options(Bt#btree{compression = Comp}, Rest); +set_options(Bt, [{cache_depth, Depth} | Rest]) when is_integer(Depth) -> + set_options(Bt#btree{cache_depth = Depth}, Rest). open(State, Fd, Options) -> {ok, set_options(#btree{root = State, fd = Fd}, Options)}. +is_btree(#btree{}) -> + true; +is_btree(_) -> + false. + get_state(#btree{root = Root}) -> Root. +get_fd(#btree{fd = Fd}) -> + Fd. + +get_reduce_fun(#btree{reduce = Reduce}) -> + Reduce. + final_reduce(#btree{reduce = Reduce}, Val) -> final_reduce(Reduce, Val); final_reduce(Reduce, {[], []}) -> @@ -89,7 +128,8 @@ fold_reduce(#btree{root = Root} = Bt, Fun, Acc, Options) -> [], KeyGroupFun, Fun, - Acc + Acc, + 0 ), if GroupedKey2 == undefined -> @@ -236,7 +276,8 @@ fold(#btree{root = Root} = Bt, Fun, Acc, Options) -> InRange, Dir, convert_fun_arity(Fun), - Acc + Acc, + 0 ); StartKey -> stream_node( @@ -247,7 +288,8 @@ fold(#btree{root = Root} = Bt, Fun, Acc, Options) -> InRange, Dir, convert_fun_arity(Fun), - Acc + Acc, + 0 ) end, case Result of @@ -276,6 +318,11 @@ query_modify(Bt, LookupKeys, InsertValues, RemoveKeys) -> ), RemoveActions = [{remove, Key, nil} || Key <- RemoveKeys], FetchActions = [{fetch, Key, nil} || Key <- LookupKeys], + + UniqueFun = fun({Op, A, _}, {Op, B, _}) -> less_eq(Bt, A, B) end, + InsertActions1 = lists:usort(UniqueFun, InsertActions), + RemoveActions1 = lists:usort(UniqueFun, RemoveActions), + SortFun = fun({OpA, A, _}, {OpB, B, _}) -> case A == B of @@ -284,8 +331,8 @@ query_modify(Bt, LookupKeys, InsertValues, RemoveKeys) -> false -> less(Bt, A, B) end end, - Actions = lists:sort(SortFun, lists:append([InsertActions, RemoveActions, FetchActions])), - {ok, KeyPointers, QueryResults} = modify_node(Bt, Root, Actions, []), + Actions = lists:sort(SortFun, lists:append([InsertActions1, RemoveActions1, FetchActions])), + {ok, KeyPointers, QueryResults} = modify_node(Bt, Root, Actions, [], 0), {ok, NewRoot} = complete_root(Bt, KeyPointers), {ok, QueryResults, Bt#btree{root = NewRoot}}. @@ -301,64 +348,87 @@ lookup(#btree{root = Root, less = Less} = Bt, Keys) -> undefined -> lists:sort(Keys); _ -> lists:sort(Less, Keys) end, - {ok, SortedResults} = lookup(Bt, Root, SortedKeys), + {ok, SortedResults} = lookup(Bt, Root, SortedKeys, 0), % We want to return the results in the same order as the keys were input % but we may have changed the order when we sorted. So we need to put the % order back into the results. couch_util:reorder_results(Keys, SortedResults). -lookup(_Bt, nil, Keys) -> +lookup(_Bt, nil, Keys, _Depth) -> {ok, [{Key, not_found} || Key <- Keys]}; -lookup(Bt, Node, Keys) -> +lookup(Bt, Node, Keys, Depth0) -> + Depth = Depth0 + 1, Pointer = element(1, Node), - {NodeType, NodeList} = get_node(Bt, Pointer), + {NodeType, NodeList} = get_node(Bt, Pointer, Depth), case NodeType of kp_node -> - lookup_kpnode(Bt, list_to_tuple(NodeList), 1, Keys, []); + lookup_kpnode(Bt, list_to_tuple(NodeList), 1, Keys, [], Depth); kv_node -> - lookup_kvnode(Bt, list_to_tuple(NodeList), 1, Keys, []) + lookup_kvnode(Bt, list_to_tuple(NodeList), 1, Keys, [], Depth) end. -lookup_kpnode(_Bt, _NodeTuple, _LowerBound, [], Output) -> +lookup_kpnode(_Bt, _NodeTuple, _LowerBound, [], Output, _Depth) -> {ok, lists:reverse(Output)}; -lookup_kpnode(_Bt, NodeTuple, LowerBound, Keys, Output) when tuple_size(NodeTuple) < LowerBound -> +lookup_kpnode(_Bt, NodeTuple, LowerBound, Keys, Output, _Depth) when + tuple_size(NodeTuple) < LowerBound +-> {ok, lists:reverse(Output, [{Key, not_found} || Key <- Keys])}; -lookup_kpnode(Bt, NodeTuple, LowerBound, [FirstLookupKey | _] = LookupKeys, Output) -> +lookup_kpnode(Bt, NodeTuple, LowerBound, [FirstLookupKey | _] = LookupKeys, Output, Depth) -> N = find_first_gteq(Bt, NodeTuple, LowerBound, tuple_size(NodeTuple), FirstLookupKey), {Key, PointerInfo} = element(N, NodeTuple), SplitFun = fun(LookupKey) -> not less(Bt, Key, LookupKey) end, case lists:splitwith(SplitFun, LookupKeys) of {[], GreaterQueries} -> - lookup_kpnode(Bt, NodeTuple, N + 1, GreaterQueries, Output); + lookup_kpnode(Bt, NodeTuple, N + 1, GreaterQueries, Output, Depth); {LessEqQueries, GreaterQueries} -> - {ok, Results} = lookup(Bt, PointerInfo, LessEqQueries), - lookup_kpnode(Bt, NodeTuple, N + 1, GreaterQueries, lists:reverse(Results, Output)) + {ok, Results} = lookup(Bt, PointerInfo, LessEqQueries, Depth), + lookup_kpnode( + Bt, NodeTuple, N + 1, GreaterQueries, lists:reverse(Results, Output), Depth + ) end. -lookup_kvnode(_Bt, _NodeTuple, _LowerBound, [], Output) -> +lookup_kvnode(_Bt, _NodeTuple, _LowerBound, [], Output, _Depth) -> {ok, lists:reverse(Output)}; -lookup_kvnode(_Bt, NodeTuple, LowerBound, Keys, Output) when tuple_size(NodeTuple) < LowerBound -> +lookup_kvnode(_Bt, NodeTuple, LowerBound, Keys, Output, _Depth) when + tuple_size(NodeTuple) < LowerBound +-> % keys not found {ok, lists:reverse(Output, [{Key, not_found} || Key <- Keys])}; -lookup_kvnode(Bt, NodeTuple, LowerBound, [LookupKey | RestLookupKeys], Output) -> +lookup_kvnode(Bt, NodeTuple, LowerBound, [LookupKey | RestLookupKeys], Output, Depth) -> N = find_first_gteq(Bt, NodeTuple, LowerBound, tuple_size(NodeTuple), LookupKey), {Key, Value} = element(N, NodeTuple), case less(Bt, LookupKey, Key) of true -> % LookupKey is less than Key - lookup_kvnode(Bt, NodeTuple, N, RestLookupKeys, [{LookupKey, not_found} | Output]); + lookup_kvnode( + Bt, NodeTuple, N, RestLookupKeys, [{LookupKey, not_found} | Output], Depth + ); false -> case less(Bt, Key, LookupKey) of true -> % LookupKey is greater than Key - lookup_kvnode(Bt, NodeTuple, N + 1, RestLookupKeys, [ - {LookupKey, not_found} | Output - ]); + lookup_kvnode( + Bt, + NodeTuple, + N + 1, + RestLookupKeys, + [ + {LookupKey, not_found} | Output + ], + Depth + ); false -> % LookupKey is equal to Key - lookup_kvnode(Bt, NodeTuple, N, RestLookupKeys, [ - {LookupKey, {ok, assemble(Bt, LookupKey, Value)}} | Output - ]) + lookup_kvnode( + Bt, + NodeTuple, + N, + RestLookupKeys, + [ + {LookupKey, {ok, assemble(Bt, LookupKey, Value)}} | Output + ], + Depth + ) end end. @@ -407,32 +477,29 @@ chunkify([InElement | RestInList], ChunkThreshold, OutList, OutListSize, OutputC -compile({inline, [get_chunk_size/0]}). get_chunk_size() -> - try - list_to_integer(config:get("couchdb", "btree_chunk_size", "1279")) - catch - error:badarg -> - 1279 - end. + config:get_integer("couchdb", "btree_chunk_size", ?DEFAULT_CHUNK_SIZE). -modify_node(Bt, RootPointerInfo, Actions, QueryOutput) -> +modify_node(Bt, RootPointerInfo, Actions, QueryOutput, Depth0) -> + Depth = Depth0 + 1, {NodeType, NodeList} = case RootPointerInfo of nil -> {kv_node, []}; _Tuple -> Pointer = element(1, RootPointerInfo), - get_node(Bt, Pointer) + get_node(Bt, Pointer, Depth) end, NodeTuple = list_to_tuple(NodeList), {ok, NewNodeList, QueryOutput2} = case NodeType of - kp_node -> modify_kpnode(Bt, NodeTuple, 1, Actions, [], QueryOutput); - kv_node -> modify_kvnode(Bt, NodeTuple, 1, Actions, [], QueryOutput) + kp_node -> modify_kpnode(Bt, NodeTuple, 1, Actions, [], QueryOutput, Depth); + kv_node -> modify_kvnode(Bt, NodeTuple, 1, Actions, [], QueryOutput, Depth) end, case NewNodeList of % no nodes remain [] -> + reset_cache_usage(Bt, RootPointerInfo, Depth), {ok, [], QueryOutput2}; % nothing changed NodeList -> @@ -444,6 +511,7 @@ modify_node(Bt, RootPointerInfo, Actions, QueryOutput) -> nil -> write_node(Bt, NodeType, NewNodeList); _ -> + reset_cache_usage(Bt, RootPointerInfo, Depth), {LastKey, _LastValue} = element(tuple_size(NodeTuple), NodeTuple), OldNode = {LastKey, RootPointerInfo}, write_node(Bt, OldNode, NodeType, NodeList, NewNodeList) @@ -470,7 +538,30 @@ reduce_tree_size(kp_node, _NodeSize, [{_K, {_P, _Red, nil}} | _]) -> reduce_tree_size(kp_node, NodeSize, [{_K, {_P, _Red, Sz}} | NodeList]) -> reduce_tree_size(kp_node, NodeSize + Sz, NodeList). -get_node(#btree{fd = Fd}, NodePos) -> +reset_cache_usage(_, nil, _Depth) -> + ok; +reset_cache_usage(#btree{cache_depth = Max}, _, Depth) when Depth > Max -> + ok; +reset_cache_usage(#btree{fd = Fd}, RootPointerInfo, _Depth) -> + Pointer = element(1, RootPointerInfo), + couch_bt_engine_cache:reset({Fd, Pointer}). + +get_node(#btree{fd = Fd, cache_depth = Max}, NodePos, Depth) when Depth =< Max -> + case couch_bt_engine_cache:lookup({Fd, NodePos}) of + undefined -> + {ok, {NodeType, NodeList}} = couch_file:pread_term(Fd, NodePos), + case NodeType of + kp_node -> + Priority = max(1, ?ROOT_NODE_CACHE_PRIORITY - Depth), + couch_bt_engine_cache:insert({Fd, NodePos}, NodeList, Priority); + kv_node -> + ok + end, + {NodeType, NodeList}; + NodeList -> + {kp_node, NodeList} + end; +get_node(#btree{fd = Fd}, NodePos, _Depth) -> {ok, {NodeType, NodeList}} = couch_file:pread_term(Fd, NodePos), {NodeType, NodeList}. @@ -546,9 +637,9 @@ old_list_is_prefix([KV | Rest1], [KV | Rest2], Acc) -> old_list_is_prefix(_OldList, _NewList, _Acc) -> false. -modify_kpnode(Bt, {}, _LowerBound, Actions, [], QueryOutput) -> - modify_node(Bt, nil, Actions, QueryOutput); -modify_kpnode(_Bt, NodeTuple, LowerBound, [], ResultNode, QueryOutput) -> +modify_kpnode(Bt, {}, _LowerBound, Actions, [], QueryOutput, Depth) -> + modify_node(Bt, nil, Actions, QueryOutput, Depth); +modify_kpnode(_Bt, NodeTuple, LowerBound, [], ResultNode, QueryOutput, _Depth) -> {ok, lists:reverse( ResultNode, @@ -566,7 +657,8 @@ modify_kpnode( LowerBound, [{_, FirstActionKey, _} | _] = Actions, ResultNode, - QueryOutput + QueryOutput, + Depth ) -> Sz = tuple_size(NodeTuple), N = find_first_gteq(Bt, NodeTuple, LowerBound, Sz, FirstActionKey), @@ -575,7 +667,7 @@ modify_kpnode( % perform remaining actions on last node {_, PointerInfo} = element(Sz, NodeTuple), {ok, ChildKPs, QueryOutput2} = - modify_node(Bt, PointerInfo, Actions, QueryOutput), + modify_node(Bt, PointerInfo, Actions, QueryOutput, Depth), NodeList = lists:reverse( ResultNode, bounded_tuple_to_list( @@ -593,7 +685,7 @@ modify_kpnode( end, {LessEqQueries, GreaterQueries} = lists:splitwith(SplitFun, Actions), {ok, ChildKPs, QueryOutput2} = - modify_node(Bt, PointerInfo, LessEqQueries, QueryOutput), + modify_node(Bt, PointerInfo, LessEqQueries, QueryOutput, Depth), ResultNode2 = lists:reverse( ChildKPs, bounded_tuple_to_revlist( @@ -603,7 +695,7 @@ modify_kpnode( ResultNode ) ), - modify_kpnode(Bt, NodeTuple, N + 1, GreaterQueries, ResultNode2, QueryOutput2) + modify_kpnode(Bt, NodeTuple, N + 1, GreaterQueries, ResultNode2, QueryOutput2, Depth) end. bounded_tuple_to_revlist(_Tuple, Start, End, Tail) when Start > End -> @@ -631,7 +723,7 @@ find_first_gteq(Bt, Tuple, Start, End, Key) -> find_first_gteq(Bt, Tuple, Start, Mid, Key) end. -modify_kvnode(_Bt, NodeTuple, LowerBound, [], ResultNode, QueryOutput) -> +modify_kvnode(_Bt, NodeTuple, LowerBound, [], ResultNode, QueryOutput, _Depth) -> {ok, lists:reverse( ResultNode, bounded_tuple_to_list(NodeTuple, LowerBound, tuple_size(NodeTuple), []) @@ -643,7 +735,8 @@ modify_kvnode( LowerBound, [{ActionType, ActionKey, ActionValue} | RestActions], ResultNode, - QueryOutput + QueryOutput, + Depth ) when LowerBound > tuple_size(NodeTuple) -> case ActionType of insert -> @@ -653,16 +746,25 @@ modify_kvnode( LowerBound, RestActions, [{ActionKey, ActionValue} | ResultNode], - QueryOutput + QueryOutput, + Depth ); remove -> % just drop the action - modify_kvnode(Bt, NodeTuple, LowerBound, RestActions, ResultNode, QueryOutput); + modify_kvnode(Bt, NodeTuple, LowerBound, RestActions, ResultNode, QueryOutput, Depth); fetch -> % the key/value must not exist in the tree - modify_kvnode(Bt, NodeTuple, LowerBound, RestActions, ResultNode, [ - {not_found, {ActionKey, nil}} | QueryOutput - ]) + modify_kvnode( + Bt, + NodeTuple, + LowerBound, + RestActions, + ResultNode, + [ + {not_found, {ActionKey, nil}} | QueryOutput + ], + Depth + ) end; modify_kvnode( Bt, @@ -670,7 +772,8 @@ modify_kvnode( LowerBound, [{ActionType, ActionKey, ActionValue} | RestActions], AccNode, - QueryOutput + QueryOutput, + Depth ) -> N = find_first_gteq(Bt, NodeTuple, LowerBound, tuple_size(NodeTuple), ActionKey), {Key, Value} = element(N, NodeTuple), @@ -686,16 +789,25 @@ modify_kvnode( N, RestActions, [{ActionKey, ActionValue} | ResultNode], - QueryOutput + QueryOutput, + Depth ); remove -> % ActionKey is less than the Key, just drop the action - modify_kvnode(Bt, NodeTuple, N, RestActions, ResultNode, QueryOutput); + modify_kvnode(Bt, NodeTuple, N, RestActions, ResultNode, QueryOutput, Depth); fetch -> % ActionKey is less than the Key, the key/value must not exist in the tree - modify_kvnode(Bt, NodeTuple, N, RestActions, ResultNode, [ - {not_found, {ActionKey, nil}} | QueryOutput - ]) + modify_kvnode( + Bt, + NodeTuple, + N, + RestActions, + ResultNode, + [ + {not_found, {ActionKey, nil}} | QueryOutput + ], + Depth + ) end; false -> % ActionKey and Key are maybe equal. @@ -709,18 +821,27 @@ modify_kvnode( N + 1, RestActions, [{ActionKey, ActionValue} | ResultNode], - QueryOutput + QueryOutput, + Depth ); remove -> modify_kvnode( - Bt, NodeTuple, N + 1, RestActions, ResultNode, QueryOutput + Bt, NodeTuple, N + 1, RestActions, ResultNode, QueryOutput, Depth ); fetch -> % ActionKey is equal to the Key, insert into the QueryOuput, but re-process the node % since an identical action key can follow it. - modify_kvnode(Bt, NodeTuple, N, RestActions, ResultNode, [ - {ok, assemble(Bt, Key, Value)} | QueryOutput - ]) + modify_kvnode( + Bt, + NodeTuple, + N, + RestActions, + ResultNode, + [ + {ok, assemble(Bt, Key, Value)} | QueryOutput + ], + Depth + ) end; true -> modify_kvnode( @@ -729,7 +850,8 @@ modify_kvnode( N + 1, [{ActionType, ActionKey, ActionValue} | RestActions], [{Key, Value} | ResultNode], - QueryOutput + QueryOutput, + Depth ) end end. @@ -745,7 +867,8 @@ reduce_stream_node( GroupedRedsAcc, _KeyGroupFun, _Fun, - Acc + Acc, + _Depth ) -> {ok, Acc, GroupedRedsAcc, GroupedKVsAcc, GroupedKey}; reduce_stream_node( @@ -759,10 +882,12 @@ reduce_stream_node( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth0 ) -> + Depth = Depth0 + 1, P = element(1, Node), - case get_node(Bt, P) of + case get_node(Bt, P, Depth) of {kp_node, NodeList} -> NodeList2 = adjust_dir(Dir, NodeList), reduce_stream_kp_node( @@ -776,7 +901,8 @@ reduce_stream_node( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ); {kv_node, KVs} -> KVs2 = adjust_dir(Dir, KVs), @@ -791,7 +917,8 @@ reduce_stream_node( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ) end. @@ -806,7 +933,8 @@ reduce_stream_kv_node( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ) -> GTEKeyStartKVs = case KeyStart of @@ -833,7 +961,8 @@ reduce_stream_kv_node( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ). reduce_stream_kv_node2( @@ -844,7 +973,8 @@ reduce_stream_kv_node2( GroupedRedsAcc, _KeyGroupFun, _Fun, - Acc + Acc, + _Depth ) -> {ok, Acc, GroupedRedsAcc, GroupedKVsAcc, GroupedKey}; reduce_stream_kv_node2( @@ -855,7 +985,8 @@ reduce_stream_kv_node2( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ) -> case GroupedKey of undefined -> @@ -867,7 +998,8 @@ reduce_stream_kv_node2( [], KeyGroupFun, Fun, - Acc + Acc, + Depth ); _ -> case KeyGroupFun(GroupedKey, Key) of @@ -880,7 +1012,8 @@ reduce_stream_kv_node2( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ); false -> case Fun(GroupedKey, {GroupedKVsAcc, GroupedRedsAcc}, Acc) of @@ -893,7 +1026,8 @@ reduce_stream_kv_node2( [], KeyGroupFun, Fun, - Acc2 + Acc2, + Depth ); {stop, Acc2} -> throw({stop, Acc2}) @@ -912,7 +1046,8 @@ reduce_stream_kp_node( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ) -> Nodes = case KeyStart of @@ -955,7 +1090,8 @@ reduce_stream_kp_node( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ). reduce_stream_kp_node2( @@ -969,7 +1105,8 @@ reduce_stream_kp_node2( [], KeyGroupFun, Fun, - Acc + Acc, + Depth ) -> {ok, Acc2, GroupedRedsAcc2, GroupedKVsAcc2, GroupedKey2} = reduce_stream_node( @@ -983,7 +1120,8 @@ reduce_stream_kp_node2( [], KeyGroupFun, Fun, - Acc + Acc, + Depth ), reduce_stream_kp_node2( Bt, @@ -996,7 +1134,8 @@ reduce_stream_kp_node2( GroupedRedsAcc2, KeyGroupFun, Fun, - Acc2 + Acc2, + Depth ); reduce_stream_kp_node2( Bt, @@ -1009,7 +1148,8 @@ reduce_stream_kp_node2( GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ) -> {Grouped0, Ungrouped0} = lists:splitwith( fun({Key, _}) -> @@ -1040,7 +1180,8 @@ reduce_stream_kp_node2( GroupedReds ++ GroupedRedsAcc, KeyGroupFun, Fun, - Acc + Acc, + Depth ), reduce_stream_kp_node2( Bt, @@ -1053,7 +1194,8 @@ reduce_stream_kp_node2( GroupedRedsAcc2, KeyGroupFun, Fun, - Acc2 + Acc2, + Depth ); [] -> {ok, Acc, GroupedReds ++ GroupedRedsAcc, GroupedKVsAcc, GroupedKey} @@ -1064,40 +1206,46 @@ adjust_dir(fwd, List) -> adjust_dir(rev, List) -> lists:reverse(List). -stream_node(Bt, Reds, Node, StartKey, InRange, Dir, Fun, Acc) -> +stream_node(Bt, Reds, Node, StartKey, InRange, Dir, Fun, Acc, Depth0) -> + Depth = Depth0 + 1, Pointer = element(1, Node), - {NodeType, NodeList} = get_node(Bt, Pointer), + {NodeType, NodeList} = get_node(Bt, Pointer, Depth), case NodeType of kp_node -> - stream_kp_node(Bt, Reds, adjust_dir(Dir, NodeList), StartKey, InRange, Dir, Fun, Acc); + stream_kp_node( + Bt, Reds, adjust_dir(Dir, NodeList), StartKey, InRange, Dir, Fun, Acc, Depth + ); kv_node -> - stream_kv_node(Bt, Reds, adjust_dir(Dir, NodeList), StartKey, InRange, Dir, Fun, Acc) + stream_kv_node( + Bt, Reds, adjust_dir(Dir, NodeList), StartKey, InRange, Dir, Fun, Acc, Depth + ) end. -stream_node(Bt, Reds, Node, InRange, Dir, Fun, Acc) -> +stream_node(Bt, Reds, Node, InRange, Dir, Fun, Acc, Depth0) -> + Depth = Depth0 + 1, Pointer = element(1, Node), - {NodeType, NodeList} = get_node(Bt, Pointer), + {NodeType, NodeList} = get_node(Bt, Pointer, Depth), case NodeType of kp_node -> - stream_kp_node(Bt, Reds, adjust_dir(Dir, NodeList), InRange, Dir, Fun, Acc); + stream_kp_node(Bt, Reds, adjust_dir(Dir, NodeList), InRange, Dir, Fun, Acc, Depth); kv_node -> - stream_kv_node2(Bt, Reds, [], adjust_dir(Dir, NodeList), InRange, Dir, Fun, Acc) + stream_kv_node2(Bt, Reds, [], adjust_dir(Dir, NodeList), InRange, Dir, Fun, Acc, Depth) end. -stream_kp_node(_Bt, _Reds, [], _InRange, _Dir, _Fun, Acc) -> +stream_kp_node(_Bt, _Reds, [], _InRange, _Dir, _Fun, Acc, _Depth) -> {ok, Acc}; -stream_kp_node(Bt, Reds, [{Key, Node} | Rest], InRange, Dir, Fun, Acc) -> +stream_kp_node(Bt, Reds, [{Key, Node} | Rest], InRange, Dir, Fun, Acc, Depth) -> Red = element(2, Node), case Fun(traverse, Key, Red, Acc) of {ok, Acc2} -> - case stream_node(Bt, Reds, Node, InRange, Dir, Fun, Acc2) of + case stream_node(Bt, Reds, Node, InRange, Dir, Fun, Acc2, Depth) of {ok, Acc3} -> - stream_kp_node(Bt, [Red | Reds], Rest, InRange, Dir, Fun, Acc3); + stream_kp_node(Bt, [Red | Reds], Rest, InRange, Dir, Fun, Acc3, Depth); {stop, LastReds, Acc3} -> {stop, LastReds, Acc3} end; {skip, Acc2} -> - stream_kp_node(Bt, [Red | Reds], Rest, InRange, Dir, Fun, Acc2); + stream_kp_node(Bt, [Red | Reds], Rest, InRange, Dir, Fun, Acc2, Depth); {stop, Acc2} -> {stop, Reds, Acc2} end. @@ -1112,7 +1260,7 @@ drop_nodes(Bt, Reds, StartKey, [{NodeKey, Node} | RestKPs]) -> {Reds, [{NodeKey, Node} | RestKPs]} end. -stream_kp_node(Bt, Reds, KPs, StartKey, InRange, Dir, Fun, Acc) -> +stream_kp_node(Bt, Reds, KPs, StartKey, InRange, Dir, Fun, Acc, Depth) -> {NewReds, NodesToStream} = case Dir of fwd -> @@ -1135,16 +1283,16 @@ stream_kp_node(Bt, Reds, KPs, StartKey, InRange, Dir, Fun, Acc) -> [] -> {ok, Acc}; [{_Key, Node} | Rest] -> - case stream_node(Bt, NewReds, Node, StartKey, InRange, Dir, Fun, Acc) of + case stream_node(Bt, NewReds, Node, StartKey, InRange, Dir, Fun, Acc, Depth) of {ok, Acc2} -> Red = element(2, Node), - stream_kp_node(Bt, [Red | NewReds], Rest, InRange, Dir, Fun, Acc2); + stream_kp_node(Bt, [Red | NewReds], Rest, InRange, Dir, Fun, Acc2, Depth); {stop, LastReds, Acc2} -> {stop, LastReds, Acc2} end end. -stream_kv_node(Bt, Reds, KVs, StartKey, InRange, Dir, Fun, Acc) -> +stream_kv_node(Bt, Reds, KVs, StartKey, InRange, Dir, Fun, Acc, Depth) -> DropFun = case Dir of fwd -> @@ -1154,11 +1302,11 @@ stream_kv_node(Bt, Reds, KVs, StartKey, InRange, Dir, Fun, Acc) -> end, {LTKVs, GTEKVs} = lists:splitwith(DropFun, KVs), AssembleLTKVs = [assemble(Bt, K, V) || {K, V} <- LTKVs], - stream_kv_node2(Bt, Reds, AssembleLTKVs, GTEKVs, InRange, Dir, Fun, Acc). + stream_kv_node2(Bt, Reds, AssembleLTKVs, GTEKVs, InRange, Dir, Fun, Acc, Depth). -stream_kv_node2(_Bt, _Reds, _PrevKVs, [], _InRange, _Dir, _Fun, Acc) -> +stream_kv_node2(_Bt, _Reds, _PrevKVs, [], _InRange, _Dir, _Fun, Acc, _Depth) -> {ok, Acc}; -stream_kv_node2(Bt, Reds, PrevKVs, [{K, V} | RestKVs], InRange, Dir, Fun, Acc) -> +stream_kv_node2(Bt, Reds, PrevKVs, [{K, V} | RestKVs], InRange, Dir, Fun, Acc, Depth) -> case InRange(K) of false -> {stop, {PrevKVs, Reds}, Acc}; @@ -1167,7 +1315,7 @@ stream_kv_node2(Bt, Reds, PrevKVs, [{K, V} | RestKVs], InRange, Dir, Fun, Acc) - case Fun(visit, AssembledKV, {PrevKVs, Reds}, Acc) of {ok, Acc2} -> stream_kv_node2( - Bt, Reds, [AssembledKV | PrevKVs], RestKVs, InRange, Dir, Fun, Acc2 + Bt, Reds, [AssembledKV | PrevKVs], RestKVs, InRange, Dir, Fun, Acc2, Depth ); {stop, Acc2} -> {stop, {PrevKVs, Reds}, Acc2} diff --git a/src/couch/src/couch_db.erl b/src/couch/src/couch_db.erl index e33e695c02..fe540ec867 100644 --- a/src/couch/src/couch_db.erl +++ b/src/couch/src/couch_db.erl @@ -67,6 +67,9 @@ set_security/2, set_user_ctx/2, + get_props/1, + update_props/3, + load_validation_funs/1, reload_validation_funs/1, @@ -152,6 +155,11 @@ % Purge client max lag window in seconds (defaulting to 24 hours) -define(PURGE_LAG_SEC, 86400). +% DB props which cannot be dynamically updated after db creation +-define(PROP_PARTITIONED, partitioned). +-define(PROP_HASH, hash). +-define(STATIC_PROPS, [?PROP_PARTITIONED, ?PROP_HASH]). + start_link(Engine, DbName, Filepath, Options) -> Arg = {Engine, DbName, Filepath, Options}, proc_lib:start_link(couch_db_updater, init, [Arg]). @@ -227,9 +235,9 @@ is_clustered(#db{}) -> is_clustered(?OLD_DB_REC = Db) -> ?OLD_DB_MAIN_PID(Db) == undefined. -is_partitioned(#db{options = Options}) -> - Props = couch_util:get_value(props, Options, []), - couch_util:get_value(partitioned, Props, false). +is_partitioned(#db{} = Db) -> + Props = get_props(Db), + couch_util:get_value(?PROP_PARTITIONED, Props, false). close(#db{} = Db) -> ok = couch_db_engine:decref(Db); @@ -254,7 +262,7 @@ monitored_by(Db) -> end. monitor(#db{main_pid = MainPid}) -> - erlang:monitor(process, MainPid). + monitor(process, MainPid). start_compact(#db{} = Db) -> gen_server:call(Db#db.main_pid, start_compact). @@ -269,7 +277,7 @@ wait_for_compaction(#db{main_pid = Pid} = Db, Timeout) -> Start = os:timestamp(), case gen_server:call(Pid, compactor_pid) of CPid when is_pid(CPid) -> - Ref = erlang:monitor(process, CPid), + Ref = monitor(process, CPid), receive {'DOWN', Ref, _, _, normal} when Timeout == infinity -> wait_for_compaction(Db, Timeout); @@ -279,7 +287,7 @@ wait_for_compaction(#db{main_pid = Pid} = Db, Timeout) -> {'DOWN', Ref, _, _, Reason} -> {error, Reason} after Timeout -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), {error, Timeout} end; _ -> @@ -420,7 +428,7 @@ purge_docs(#db{main_pid = Pid} = Db, UUIDsIdsRevs, Options) -> % Gather any existing purges with the same UUIDs UUIDs = element(1, lists:unzip3(UUIDsIdsRevs1)), Old1 = get_purge_infos(Db, UUIDs), - Old2 = maps:from_list([{UUID, {Id, Revs}} || {_, UUID, Id, Revs} <- Old1]), + Old2 = #{UUID => {Id, Revs} || {_, UUID, Id, Revs} <- Old1}, % Filter out all the purges which have already been processed FilterCheckFun = fun({UUID, Id, Revs}) -> case maps:is_key(UUID, Old2) of @@ -478,7 +486,7 @@ get_minimum_purge_seq(#db{} = Db) -> CS when is_integer(CS) -> case purge_client_exists(DbName, DocId, Props) of true -> - {ok, erlang:min(CS, SeqAcc)}; + {ok, min(CS, SeqAcc)}; false -> Fmt1 = "Missing or stale purge doc '~s' on ~p " @@ -489,7 +497,7 @@ get_minimum_purge_seq(#db{} = Db) -> _ -> Fmt2 = "Invalid purge doc '~s' on ~p with purge_seq '~w'", couch_log:error(Fmt2, [DocId, DbName, ClientSeq]), - {ok, erlang:min(OldestPurgeSeq, SeqAcc)} + {ok, min(OldestPurgeSeq, SeqAcc)} end; _ -> {stop, SeqAcc} @@ -503,7 +511,7 @@ get_minimum_purge_seq(#db{} = Db) -> FinalSeq = case MinIdxSeq < PurgeSeq - PurgeInfosLimit of true -> MinIdxSeq; - false -> erlang:max(0, PurgeSeq - PurgeInfosLimit) + false -> max(0, PurgeSeq - PurgeInfosLimit) end, % Log a warning if we've got a purge sequence exceeding the % configured threshold. @@ -626,11 +634,7 @@ get_db_info(Db) -> undefined -> null; Else1 -> Else1 end, - Props = - case couch_db_engine:get_props(Db) of - undefined -> null; - Else2 -> {Else2} - end, + Props = get_props(Db), InfoList = [ {db_name, Name}, {engine, couch_db_engine:get_engine(Db)}, @@ -644,7 +648,7 @@ get_db_info(Db) -> {disk_format_version, DiskVersion}, {committed_update_seq, CommittedUpdateSeq}, {compacted_seq, CompactedSeq}, - {props, Props}, + {props, {Props}}, {uuid, Uuid} ], {ok, InfoList}. @@ -837,6 +841,24 @@ set_revs_limit(#db{main_pid = Pid} = Db, Limit) when Limit > 0 -> set_revs_limit(_Db, _Limit) -> throw(invalid_revs_limit). +get_props(#db{options = Options}) -> + couch_util:get_value(props, Options, []). + +update_props(#db{main_pid = Pid} = Db, K, V) -> + check_is_admin(Db), + case lists:member(K, ?STATIC_PROPS) of + true -> + throw({bad_request, <<"cannot update static property">>}); + false -> + Props = get_props(Db), + Props1 = + case V of + undefined -> lists:keydelete(K, 1, Props); + _ -> lists:keystore(K, 1, Props, {K, V}) + end, + gen_server:call(Pid, {set_props, Props1}, infinity) + end. + name(#db{name = Name}) -> Name; name(?OLD_DB_REC = Db) -> @@ -977,7 +999,7 @@ load_validation_funs(#db{main_pid = Pid, name = <<"shards/", _/binary>>} = Db) - Funs; {'DOWN', Ref, _, _, {database_does_not_exist, _StackTrace}} -> ok = couch_server:close_db_if_idle(Db#db.name), - erlang:error(database_does_not_exist); + error(database_does_not_exist); {'DOWN', Ref, _, _, Reason} -> couch_log:error("could not load validation funs ~p", [Reason]), throw(internal_server_error) @@ -1255,7 +1277,7 @@ new_revid(#doc{body = Body, revs = {OldStart, OldRevs}, atts = Atts, deleted = D case DigestedAtts of Atts2 when length(Atts) =/= length(Atts2) -> % We must have old style non-md5 attachments - ?l2b(integer_to_list(couch_util:rand32())); + integer_to_binary(couch_util:rand32()); Atts2 -> OldRev = case OldRevs of @@ -1340,8 +1362,13 @@ update_docs(Db, Docs0, Options, ?REPLICATED_CHANGES) -> {ok, DocErrors}; update_docs(Db, Docs0, Options, ?INTERACTIVE_EDIT) -> BlockInteractiveDatabaseWrites = couch_disk_monitor:block_interactive_database_writes(), + InternalReplication = + case get(io_priority) of + {internal_repl, _} -> true; + _Else -> false + end, if - BlockInteractiveDatabaseWrites -> + not InternalReplication andalso BlockInteractiveDatabaseWrites -> {ok, [{insufficient_storage, <<"database_dir is too full">>} || _ <- Docs0]}; true -> update_docs_interactive(Db, Docs0, Options) @@ -1466,7 +1493,7 @@ write_and_commit( ) -> DocBuckets = prepare_doc_summaries(Db, DocBuckets1), ReplicatedChanges = lists:member(?REPLICATED_CHANGES, Options), - MRef = erlang:monitor(process, Pid), + MRef = monitor(process, Pid), try Pid ! {update_docs, self(), DocBuckets, LocalDocs, ReplicatedChanges}, case collect_results_with_metrics(Pid, MRef, []) of @@ -1490,7 +1517,7 @@ write_and_commit( end end after - erlang:demonitor(MRef, [flush]) + demonitor(MRef, [flush]) end. prepare_doc_summaries(Db, BucketList) -> @@ -1751,12 +1778,12 @@ validate_epochs(Epochs) -> %% Assert uniqueness. case length(Epochs) == length(lists:ukeysort(2, Epochs)) of true -> ok; - false -> erlang:error(duplicate_epoch) + false -> error(duplicate_epoch) end, %% Assert order. case Epochs == lists:sort(fun({_, A}, {_, B}) -> B =< A end, Epochs) of true -> ok; - false -> erlang:error(epoch_order) + false -> error({epoch_order, Epochs}) end. is_prefix(Pattern, Subject) -> @@ -2352,7 +2379,10 @@ is_owner_test() -> ?assertNot(is_owner(bar, 99, [{baz, 200}, {bar, 100}, {foo, 1}])), ?assertNot(is_owner(baz, 199, [{baz, 200}, {bar, 100}, {foo, 1}])), ?assertError(duplicate_epoch, validate_epochs([{foo, 1}, {bar, 1}])), - ?assertError(epoch_order, validate_epochs([{foo, 100}, {bar, 200}])). + ?assertError( + {epoch_order, [{foo, 100}, {bar, 200}]}, + validate_epochs([{foo, 100}, {bar, 200}]) + ). to_binary(DbName) when is_list(DbName) -> ?l2b(DbName); diff --git a/src/couch/src/couch_db_updater.erl b/src/couch/src/couch_db_updater.erl index 3f6c8886dc..b6dfc818da 100644 --- a/src/couch/src/couch_db_updater.erl +++ b/src/couch/src/couch_db_updater.erl @@ -92,6 +92,12 @@ handle_call({set_purge_infos_limit, Limit}, _From, Db) -> {ok, Db2} = couch_db_engine:set_purge_infos_limit(Db, Limit), ok = couch_server:db_updated(Db2), {reply, ok, Db2}; +handle_call({set_props, Props}, _From, Db) -> + {ok, Db1} = couch_db_engine:set_props(Db, Props), + Db2 = options_set_props(Db1, Props), + {ok, Db3} = couch_db_engine:commit_data(Db2), + ok = couch_server:db_updated(Db3), + {reply, ok, Db3}; handle_call({purge_docs, [], _}, _From, Db) -> {reply, {ok, []}, Db}; handle_call({purge_docs, PurgeReqs0, Options}, _From, Db) -> @@ -308,13 +314,17 @@ init_db(DbName, FilePath, EngineState, Options) -> after_doc_read = ADR }, - DbProps = couch_db_engine:get_props(InitDb), - - InitDb#db{ + Db = InitDb#db{ committed_update_seq = couch_db_engine:get_update_seq(InitDb), security = couch_db_engine:get_security(InitDb), - options = lists:keystore(props, 1, NonCreateOpts, {props, DbProps}) - }. + options = NonCreateOpts + }, + DbProps = couch_db_engine:get_props(Db), + options_set_props(Db, DbProps). + +options_set_props(#db{options = Options} = Db, Props) -> + Options1 = lists:keystore(props, 1, Options, {props, Props}), + Db#db{options = Options1}. refresh_validate_doc_funs(#db{name = <<"shards/", _/binary>> = Name} = Db) -> spawn(fabric, reset_validation_funs, [mem3:dbname(Name)]), @@ -795,22 +805,22 @@ purge_docs(Db, PurgeReqs) -> FDIs = couch_db_engine:open_docs(Db, Ids), USeq = couch_db_engine:get_update_seq(Db), - IdFDIs = lists:zip(Ids, FDIs), + IdFDIs = maps:from_list(lists:zip(Ids, FDIs)), {NewIdFDIs, Replies} = apply_purge_reqs(PurgeReqs, IdFDIs, USeq, []), - Pairs = lists:flatmap( - fun({DocId, OldFDI}) -> - {DocId, NewFDI} = lists:keyfind(DocId, 1, NewIdFDIs), - case {OldFDI, NewFDI} of - {not_found, not_found} -> - []; - {#full_doc_info{} = A, #full_doc_info{} = A} -> - []; - {#full_doc_info{}, _} -> - [{OldFDI, NewFDI}] - end - end, - IdFDIs + Pairs = lists:sort( + maps:fold( + fun(DocId, OldFDI, Acc) -> + #{DocId := NewFDI} = NewIdFDIs, + case {OldFDI, NewFDI} of + {not_found, not_found} -> Acc; + {#full_doc_info{} = A, #full_doc_info{} = A} -> Acc; + {#full_doc_info{}, _} -> [{OldFDI, NewFDI} | Acc] + end + end, + [], + IdFDIs + ) ), PSeq = couch_db_engine:get_purge_seq(Db), @@ -834,7 +844,7 @@ apply_purge_reqs([], IdFDIs, _USeq, Replies) -> {IdFDIs, lists:reverse(Replies)}; apply_purge_reqs([Req | RestReqs], IdFDIs, USeq, Replies) -> {_UUID, DocId, Revs} = Req, - {value, {_, FDI0}, RestIdFDIs} = lists:keytake(DocId, 1, IdFDIs), + #{DocId := FDI0} = IdFDIs, {NewFDI, RemovedRevs, NewUSeq} = case FDI0 of #full_doc_info{rev_tree = Tree} -> @@ -872,9 +882,8 @@ apply_purge_reqs([Req | RestReqs], IdFDIs, USeq, Replies) -> % Not found means nothing to change {not_found, [], USeq} end, - NewIdFDIs = [{DocId, NewFDI} | RestIdFDIs], NewReplies = [{ok, RemovedRevs} | Replies], - apply_purge_reqs(RestReqs, NewIdFDIs, NewUSeq, NewReplies). + apply_purge_reqs(RestReqs, IdFDIs#{DocId := NewFDI}, NewUSeq, NewReplies). commit_data(Db) -> {ok, Db1} = couch_db_engine:commit_data(Db), diff --git a/src/couch/src/couch_debug.erl b/src/couch/src/couch_debug.erl index ff864210f2..2cc0a94b41 100644 --- a/src/couch/src/couch_debug.erl +++ b/src/couch/src/couch_debug.erl @@ -48,7 +48,8 @@ dead_nodes/1, ping/1, ping/2, - ping_nodes/0, + ping_live_cluster_nodes/0, + ping_live_cluster_nodes/1, ping_nodes/1, ping_nodes/2, node_events/0 @@ -57,9 +58,12 @@ -export([ print_table/2, print_report/1, - print_report_with_info_width/2 + print_report_with_info_width/2, + print_tree/2 ]). +-define(PING_TIMEOUT_IN_MS, 60000). + -type throw(_Reason) :: no_return(). -type process_name() :: atom(). @@ -83,18 +87,23 @@ help() -> process_name, get_pid, link_tree, - mapfold, - map, - fold, + mapfold_tree, + fold_tree, + map_tree, linked_processes_info, print_linked_processes, memory_info, + resource_hoggers, + resource_hoggers_snapshot, + analyze_resource_hoggers, print_table, print_report, print_report_with_info_width, + print_tree, restart, restart_busy, dead_nodes, + ping, ping_nodes, node_events ]. @@ -230,7 +239,7 @@ help(link_tree) -> The function doesn't recurse to pids older than initial one. The Pids which are lesser than initial Pid are still shown in the output. The info argument is a list of process_info_item() as documented in - erlang:process_info/2. We don't do any attempts to prevent dangerous items. + process_info/2. We don't do any attempts to prevent dangerous items. Be warn that passing some of them such as `messages` for example can be dangerous in a very busy system. --- @@ -288,7 +297,7 @@ help(linked_processes_info) -> use of link_tree. - Pid: initial Pid to start from - Info: a list of process_info_item() as documented - in erlang:process_info/2. + in process_info/2. --- ", []); @@ -467,18 +476,27 @@ help(ping) -> Ping a node and return either a time in microseconds or an error term. + --- + ", []); +help(ping_live_cluster_nodes) -> + io:format(" + ping_live_cluster_nodes() + ping_live_cluster_nodes(Timeout) + -------------------------------- + + Ping the currently connected cluster nodes. Returns a list of + {Node, Result} tuples or an empty list. + --- ", []); help(ping_nodes) -> io:format(" - ping_nodes() - ping_nodes(Timeout) + ping_nodes(Nodes) ping_nodes(Nodes, Timeout) -------------------------------- - Ping the list of currently connected nodes. Return a list of {Node, - Result} tuples where Result is either a time in microseconds or an - error term. + Ping the list of nodes. Return a list of {Node, Result} tuples where + Result is either a time in microseconds or an error term. --- ", []); @@ -768,6 +786,7 @@ info_size(InfoKV) -> {binary, BinInfos} -> lists:sum([S || {_, S, _} <- BinInfos]); {_, V} -> V end. + resource_hoggers(MemoryInfo, InfoKey) -> KeyFun = fun ({_Pid, _Id, undefined}) -> undefined; @@ -976,12 +995,15 @@ ping(Node) -> ping(Node, Timeout) -> mem3:ping(Node, Timeout). -ping_nodes() -> +ping_live_cluster_nodes() -> mem3:ping_nodes(). -ping_nodes(Timeout) -> +ping_live_cluster_nodes(Timeout) -> mem3:ping_nodes(Timeout). +ping_nodes(Nodes) -> + mem3:ping_nodes(Nodes, ?PING_TIMEOUT_IN_MS). + ping_nodes(Nodes, Timeout) -> mem3:ping_nodes(Nodes, Timeout). @@ -1007,6 +1029,7 @@ print_table(Rows, TableSpec) -> end, Rows ), + io:format("~n", []), ok. print_report(Report) -> @@ -1124,8 +1147,8 @@ random_processes(Acc, Depth) -> end); open_port -> spawn_link(fun() -> - Port = erlang:open_port({spawn, "sleep 10"}, [hide]), - true = erlang:link(Port), + Port = open_port({spawn, "sleep 10"}, [hide]), + true = link(Port), Caller ! {Ref, random_processes(Depth - 1)}, receive looper -> ok diff --git a/src/couch/src/couch_doc.erl b/src/couch/src/couch_doc.erl index 7b867f08d1..7ad0f76b63 100644 --- a/src/couch/src/couch_doc.erl +++ b/src/couch/src/couch_doc.erl @@ -43,7 +43,7 @@ to_branch(Doc, [RevId | Rest]) -> to_json_rev(0, []) -> []; to_json_rev(Start, [FirstRevId | _]) -> - [{<<"_rev">>, ?l2b([integer_to_list(Start), "-", revid_to_str(FirstRevId)])}]. + [{<<"_rev">>, rev_to_str({Start, FirstRevId})}]. to_json_body(true, {Body}) -> Body ++ [{<<"_deleted">>, true}]; @@ -75,11 +75,13 @@ to_json_revisions(Options, Start, RevIds0) -> revid_to_str(RevId) when size(RevId) =:= 16 -> couch_util:to_hex_bin(RevId); -revid_to_str(RevId) -> - RevId. +revid_to_str(RevId) when is_binary(RevId) -> + RevId; +revid_to_str(RevId) when is_list(RevId) -> + list_to_binary(RevId). rev_to_str({Pos, RevId}) -> - ?l2b([integer_to_list(Pos), "-", revid_to_str(RevId)]). + <<(integer_to_binary(Pos))/binary, $-, (revid_to_str(RevId))/binary>>. revs_to_strs([]) -> []; @@ -95,7 +97,7 @@ to_json_meta(Meta) -> JsonObj = {[ {<<"rev">>, rev_to_str({PosAcc, RevId})}, - {<<"status">>, ?l2b(atom_to_list(Status))} + {<<"status">>, atom_to_binary(Status)} ]}, {JsonObj, PosAcc - 1} end, @@ -186,12 +188,10 @@ from_json_obj({Props}, DbName) -> from_json_obj(_Other, _) -> throw({bad_request, "Document must be a JSON object"}). -parse_revid(RevId) when size(RevId) =:= 32 -> - RevInt = erlang:list_to_integer(?b2l(RevId), 16), - <>; -parse_revid(RevId) when length(RevId) =:= 32 -> - RevInt = erlang:list_to_integer(RevId, 16), - <>; +parse_revid(RevId) when is_binary(RevId), size(RevId) =:= 32 -> + binary:decode_hex(RevId); +parse_revid(RevId) when is_list(RevId), length(RevId) =:= 32 -> + binary:decode_hex(list_to_binary(RevId)); parse_revid(RevId) when is_binary(RevId) -> RevId; parse_revid(RevId) when is_list(RevId) -> @@ -388,13 +388,13 @@ max_seq(Tree, UpdateSeq) -> case Value of {_Deleted, _DiskPos, OldTreeSeq} -> % Older versions didn't track data sizes. - erlang:max(MaxOldSeq, OldTreeSeq); + max(MaxOldSeq, OldTreeSeq); % necessary clause? {_Deleted, _DiskPos, OldTreeSeq, _Size} -> % Older versions didn't store #leaf records. - erlang:max(MaxOldSeq, OldTreeSeq); + max(MaxOldSeq, OldTreeSeq); #leaf{seq = OldTreeSeq} -> - erlang:max(MaxOldSeq, OldTreeSeq); + max(MaxOldSeq, OldTreeSeq); _ -> MaxOldSeq end @@ -566,7 +566,7 @@ restart_open_doc_revs(Parser, Ref, NewRef) -> unlink(Parser), exit(Parser, kill), flush_parser_messages(Ref), - erlang:error({restart_open_doc_revs, NewRef}). + error({restart_open_doc_revs, NewRef}). flush_parser_messages(Ref) -> receive diff --git a/src/couch/src/couch_file.erl b/src/couch/src/couch_file.erl index c1a069edd4..2d0920f7e8 100644 --- a/src/couch/src/couch_file.erl +++ b/src/couch/src/couch_file.erl @@ -47,11 +47,11 @@ -export([append_term/2, append_term/3]). -export([pread_terms/2]). -export([append_terms/2, append_terms/3]). --export([write_header/2, read_header/1]). +-export([write_header/2, write_header/3, read_header/1]). -export([delete/2, delete/3, nuke_dir/2, init_delete_dir/1]). % gen_server callbacks --export([init/1, terminate/2, format_status/2]). +-export([init/1, terminate/2]). -export([handle_call/3, handle_cast/2, handle_info/2]). %% helper functions @@ -290,20 +290,20 @@ sync(Filepath) when is_list(Filepath) -> ok -> ok; {error, Reason} -> - erlang:error({fsync_error, Reason}) + error({fsync_error, Reason}) end after ok = file:close(Fd) end; {error, Error} -> - erlang:error(Error) + error(Error) end; sync(Fd) -> case gen_server:call(Fd, sync, infinity) of ok -> ok; {error, Reason} -> - erlang:error({fsync_error, Reason}) + error({fsync_error, Reason}) end. %%---------------------------------------------------------------------- @@ -410,9 +410,7 @@ delete_dir(RootDelDir, Dir) -> init_delete_dir(RootDir) -> Dir = filename:join(RootDir, ".delete"), - % note: ensure_dir requires an actual filename companent, which is the - % reason for "foo". - filelib:ensure_dir(filename:join(Dir, "foo")), + filelib:ensure_path(Dir), spawn(fun() -> filelib:fold_files( Dir, @@ -435,11 +433,16 @@ read_header(Fd) -> end. write_header(Fd, Data) -> + write_header(Fd, Data, []). + +% Only the sync option is currently supported +% +write_header(Fd, Data, Opts) when is_list(Opts) -> Bin = ?term_to_bin(Data), Checksum = generate_checksum(Bin), % now we assemble the final header binary and write to disk FinalBin = <>, - ioq:call(Fd, {write_header, FinalBin}, erlang:get(io_priority)). + ioq:call(Fd, {write_header, FinalBin, Opts}, erlang:get(io_priority)). init_status_error(ReturnPid, Ref, Error) -> ReturnPid ! {Ref, self(), Error}, @@ -582,20 +585,21 @@ handle_call({append_bins, Bins}, _From, #file{} = File) -> {{ok, Resps}, File1} -> {reply, {ok, Resps}, File1}; {Error, File1} -> {reply, Error, File1} end; -handle_call({write_header, Bin}, _From, #file{fd = Fd, eof = Pos} = File) -> - BinSize = byte_size(Bin), - case Pos rem ?SIZE_BLOCK of - 0 -> - Padding = <<>>; - BlockOffset -> - Padding = <<0:(8 * (?SIZE_BLOCK - BlockOffset))>> - end, - FinalBin = [Padding, <<1, BinSize:32/integer>> | make_blocks(5, [Bin])], - case file:write(Fd, FinalBin) of - ok -> - {reply, ok, File#file{eof = Pos + iolist_size(FinalBin)}}; - Error -> - {reply, Error, reset_eof(File)} +handle_call({write_header, Bin, Opts}, _From, #file{} = File) -> + try + ok = header_fsync(File, Opts), + case handle_write_header(Bin, File) of + {ok, NewFile} -> + ok = header_fsync(NewFile, Opts), + {reply, ok, NewFile}; + {{error, Err}, NewFile} -> + {reply, {error, Err}, NewFile} + end + catch + error:{fsync_error, Error} -> + % If fsync error happens we stop. See comment in + % handle_call(sync, ...) why we're dropping the fd + {stop, {error, Error}, {error, Error}, #file{fd = nil}} end; handle_call(find_header, _From, #file{fd = Fd, eof = Pos} = File) -> {reply, find_header(Fd, Pos div ?SIZE_BLOCK), File}. @@ -617,10 +621,6 @@ handle_info({'DOWN', Ref, process, _Pid, _Info}, #file{db_monitor = Ref} = File) false -> {noreply, File} end. -format_status(_Opt, [PDict, #file{} = File]) -> - {_Fd, FilePath} = couch_util:get_value(couch_file_fd, PDict), - [{data, [{"State", File}, {"InitialFilePath", FilePath}]}]. - eof(#file{fd = Fd}) -> file:position(Fd, eof). @@ -661,6 +661,17 @@ pread(#file{} = File, PosL) -> Extracted = lists:zipwith(ZipFun, DataSizes, Resps), {ok, Extracted}. +header_fsync(#file{fd = Fd}, Opts) when is_list(Opts) -> + case proplists:get_value(sync, Opts) of + true -> + case fsync(Fd) of + ok -> ok; + {error, Err} -> error({fsync_error, Err}) + end; + _ -> + ok + end. + fsync(Fd) -> T0 = erlang:monotonic_time(), % We do not rely on mtime/atime for our safety/consitency so we can use @@ -759,6 +770,18 @@ find_newest_header(Fd, [{Location, Size} | LocationSizes]) -> find_newest_header(Fd, LocationSizes) end. +handle_write_header(Bin, #file{fd = Fd, eof = Pos} = File) -> + BinSize = byte_size(Bin), + case Pos rem ?SIZE_BLOCK of + 0 -> Padding = <<>>; + BlockOffset -> Padding = <<0:(8 * (?SIZE_BLOCK - BlockOffset))>> + end, + FinalBin = [Padding, <<1, BinSize:32/integer>> | make_blocks(5, [Bin])], + case file:write(Fd, FinalBin) of + ok -> {ok, File#file{eof = Pos + iolist_size(FinalBin)}}; + {error, Error} -> {{error, Error}, reset_eof(File)} + end. + read_multi_raw_iolists_int(#file{fd = Fd, eof = Eof} = File, PosLens) -> MapFun = fun({Pos, Len}) -> get_pread_locnum(File, Pos, Len) end, LocNums = lists:map(MapFun, PosLens), diff --git a/src/couch/src/couch_flags_config.erl b/src/couch/src/couch_flags_config.erl index a50f4411f9..4281f2266e 100644 --- a/src/couch/src/couch_flags_config.erl +++ b/src/couch/src/couch_flags_config.erl @@ -134,7 +134,7 @@ parse_flags(_Tokens, _) -> parse_flags_term(FlagsBin) -> {Flags, Errors} = lists:splitwith( - fun erlang:is_atom/1, + fun is_atom/1, [parse_flag(F) || F <- split_by_comma(FlagsBin)] ), case Errors of diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl index 4566157da0..7c6a60d2b9 100644 --- a/src/couch/src/couch_httpd_auth.erl +++ b/src/couch/src/couch_httpd_auth.erl @@ -381,7 +381,7 @@ cookie_authentication_handler(#httpd{mochi_req = MochiReq} = Req, AuthModule) -> "timeout", 600 ), couch_log:debug("timeout ~p", [Timeout]), - case (catch erlang:list_to_integer(TimeStr, 16)) of + case (catch list_to_integer(TimeStr, 16)) of TimeStamp when CurrentTime < TimeStamp + Timeout -> case lists:any(VerifyHash, HashAlgorithms) of true -> @@ -438,7 +438,7 @@ cookie_auth_header(_Req, _Headers) -> []. cookie_auth_cookie(Req, User, Secret, TimeStamp) -> - SessionItems = [User, erlang:integer_to_list(TimeStamp, 16)], + SessionItems = [User, integer_to_list(TimeStamp, 16)], cookie_auth_cookie(Req, Secret, SessionItems). cookie_auth_cookie(Req, Secret, SessionItems) when is_list(SessionItems) -> diff --git a/src/couch/src/couch_httpd_db.erl b/src/couch/src/couch_httpd_db.erl index 5c566ed789..733991d842 100644 --- a/src/couch/src/couch_httpd_db.erl +++ b/src/couch/src/couch_httpd_db.erl @@ -817,7 +817,7 @@ receive_request_data(Req) -> receive_request_data(Req, couch_httpd:body_length(Req)). receive_request_data(Req, LenLeft) when LenLeft > 0 -> - Len = erlang:min(4096, LenLeft), + Len = min(4096, LenLeft), Data = couch_httpd:recv(Req, Len), {Data, fun() -> receive_request_data(Req, LenLeft - iolist_size(Data)) end}; receive_request_data(_Req, _) -> diff --git a/src/couch/src/couch_httpd_multipart.erl b/src/couch/src/couch_httpd_multipart.erl index 80fc48a75f..87c588fc8d 100644 --- a/src/couch/src/couch_httpd_multipart.erl +++ b/src/couch/src/couch_httpd_multipart.erl @@ -27,7 +27,7 @@ decode_multipart_stream(ContentType, DataFun, Ref) -> Parent = self(), NumMpWriters = num_mp_writers(), {Parser, ParserRef} = spawn_monitor(fun() -> - ParentRef = erlang:monitor(process, Parent), + ParentRef = monitor(process, Parent), put(mp_parent_ref, ParentRef), num_mp_writers(NumMpWriters), {<<"--", _/binary>>, _, _} = couch_httpd:parse_multipart_request( @@ -214,7 +214,7 @@ maybe_send_data({Ref, Chunks, Offset, Counters, Waiting}) -> end. handle_hello(WriterPid, Counters) -> - WriterRef = erlang:monitor(process, WriterPid), + WriterRef = monitor(process, WriterPid), orddict:store(WriterPid, {WriterRef, 0}, Counters). update_writer(WriterPid, Counters) -> @@ -222,7 +222,7 @@ update_writer(WriterPid, Counters) -> {ok, {WriterRef, Count}} -> orddict:store(WriterPid, {WriterRef, Count + 1}, Counters); error -> - WriterRef = erlang:monitor(process, WriterPid), + WriterRef = monitor(process, WriterPid), orddict:store(WriterPid, {WriterRef, 1}, Counters) end. @@ -274,7 +274,7 @@ atts_to_mp( WriteFun, AttFun ) -> - LengthBin = list_to_binary(integer_to_list(Len)), + LengthBin = integer_to_binary(Len), % write headers WriteFun(<<"\r\nContent-Disposition: attachment; filename=\"", Name/binary, "\"">>), WriteFun(<<"\r\nContent-Type: ", Type/binary>>), @@ -344,7 +344,7 @@ length_multipart_stream(Boundary, JsonBytes, Atts) -> end. abort_multipart_stream(Parser) -> - MonRef = erlang:monitor(process, Parser), + MonRef = monitor(process, Parser), Parser ! abort_parsing, receive {'DOWN', MonRef, _, _, _} -> ok diff --git a/src/couch/src/couch_httpd_vhost.erl b/src/couch/src/couch_httpd_vhost.erl index 170e1a2f74..91da711afe 100644 --- a/src/couch/src/couch_httpd_vhost.erl +++ b/src/couch/src/couch_httpd_vhost.erl @@ -329,7 +329,7 @@ split_host_port(HostAsString) -> N -> HostPart = string:substr(HostAsString, 1, N - 1), case - (catch erlang:list_to_integer( + (catch list_to_integer( string:substr( HostAsString, N + 1, diff --git a/src/couch/src/couch_key_tree.erl b/src/couch/src/couch_key_tree.erl index 87504c78c7..6a3dffc040 100644 --- a/src/couch/src/couch_key_tree.erl +++ b/src/couch/src/couch_key_tree.erl @@ -113,7 +113,7 @@ merge_tree([{Depth, Nodes} | Rest], {IDepth, INodes} = Tree, MergeAcc) -> % value that's used throughout this module. case merge_at([Nodes], Depth - IDepth, [INodes]) of {[Merged], Result} -> - NewDepth = erlang:min(Depth, IDepth), + NewDepth = min(Depth, IDepth), {Rest ++ [{NewDepth, Merged} | MergeAcc], Result}; fail -> merge_tree(Rest, Tree, [{Depth, Nodes} | MergeAcc]) @@ -507,12 +507,12 @@ stem_tree(Depth, {Key, Val, Children}, Limit, Seen0) -> {SeenAcc, LimitPosAcc, ChildAcc, BranchAcc} = Acc, case stem_tree(Depth + 1, Child, Limit, SeenAcc) of {NewSeenAcc, LimitPos, NewChild, NewBranches} -> - NewLimitPosAcc = erlang:max(LimitPos, LimitPosAcc), + NewLimitPosAcc = max(LimitPos, LimitPosAcc), NewChildAcc = [NewChild | ChildAcc], NewBranchAcc = NewBranches ++ BranchAcc, {NewSeenAcc, NewLimitPosAcc, NewChildAcc, NewBranchAcc}; {NewSeenAcc, LimitPos, NewBranches} -> - NewLimitPosAcc = erlang:max(LimitPos, LimitPosAcc), + NewLimitPosAcc = max(LimitPos, LimitPosAcc), NewBranchAcc = NewBranches ++ BranchAcc, {NewSeenAcc, NewLimitPosAcc, ChildAcc, NewBranchAcc} end diff --git a/src/couch/src/couch_multidb_changes.erl b/src/couch/src/couch_multidb_changes.erl index 2ed200314d..bb8a3fe47c 100644 --- a/src/couch/src/couch_multidb_changes.erl +++ b/src/couch/src/couch_multidb_changes.erl @@ -241,7 +241,7 @@ should_wait_for_shard_map(<<_/binary>>) -> -spec register_with_event_server(pid()) -> reference(). register_with_event_server(Server) -> - Ref = erlang:monitor(process, couch_event_server), + Ref = monitor(process, couch_event_server), couch_event:register_all(Server), Ref. @@ -475,7 +475,7 @@ setup_all() -> meck:expect(couch_db, close, 1, ok), mock_changes_reader(), % create process to stand in for couch_event_server - % mocking erlang:monitor doesn't work, so give it real process to monitor + % mocking monitor doesn't work, so give it real process to monitor EvtPid = spawn_link(fun() -> receive looper -> ok @@ -709,7 +709,7 @@ t_handle_info_change_feed_exited_and_need_rescan(_) -> t_spawn_changes_reader(_) -> Pid = start_changes_reader(?DBNAME, 3), - ?assert(erlang:is_process_alive(Pid)), + ?assert(is_process_alive(Pid)), ChArgs = kill_mock_changes_reader_and_get_its_args(Pid), ?assertEqual({self(), ?DBNAME}, ChArgs), ?assert(meck:validate(couch_db)), @@ -1135,7 +1135,7 @@ kill_mock_changes_reader_and_get_its_args(Pid) -> {'DOWN', Ref, _, Pid, {Server, DbName}} -> {Server, DbName} after 1000 -> - erlang:error(spawn_change_reader_timeout) + error(spawn_change_reader_timeout) end. mock_changes_reader() -> diff --git a/src/couch/src/couch_native_process.erl b/src/couch/src/couch_native_process.erl index c9280f1d76..9d555c8a42 100644 --- a/src/couch/src/couch_native_process.erl +++ b/src/couch/src/couch_native_process.erl @@ -111,7 +111,7 @@ handle_call({prompt, Data}, _From, State) -> end. handle_cast(garbage_collect, State) -> - erlang:garbage_collect(), + garbage_collect(), {noreply, State, State#evstate.idle}; handle_cast(stop, State) -> {stop, normal, State}; @@ -120,7 +120,7 @@ handle_cast(_Msg, State) -> handle_info(timeout, State) -> couch_proc_manager:os_proc_idle(self()), - erlang:garbage_collect(), + garbage_collect(), {noreply, State, State#evstate.idle}; handle_info({'EXIT', _, normal}, State) -> {noreply, State, State#evstate.idle}; @@ -478,6 +478,6 @@ to_binary(true) -> to_binary(false) -> false; to_binary(Data) when is_atom(Data) -> - list_to_binary(atom_to_list(Data)); + atom_to_binary(Data); to_binary(Data) -> Data. diff --git a/src/couch/src/couch_os_process.erl b/src/couch/src/couch_os_process.erl index 59ceeca13a..339c93f72f 100644 --- a/src/couch/src/couch_os_process.erl +++ b/src/couch/src/couch_os_process.erl @@ -152,7 +152,7 @@ init([Command]) -> couch_stats:increment_counter([couchdb, query_server, process_starts]), spawn(fun() -> % this ensure the real os process is killed when this process dies. - erlang:monitor(process, Pid), + monitor(process, Pid), killer(OsPid) end), {ok, OsProc, IdleLimit}. @@ -191,7 +191,7 @@ handle_call({prompt, Data}, _From, #os_proc{idle = Idle} = OsProc) -> end. handle_cast(garbage_collect, #os_proc{idle = Idle} = OsProc) -> - erlang:garbage_collect(), + garbage_collect(), {noreply, OsProc, Idle}; handle_cast(stop, OsProc) -> {stop, normal, OsProc}; @@ -201,7 +201,7 @@ handle_cast(Msg, #os_proc{idle = Idle} = OsProc) -> handle_info(timeout, #os_proc{idle = Idle} = OsProc) -> couch_proc_manager:os_proc_idle(self()), - erlang:garbage_collect(), + garbage_collect(), {noreply, OsProc, Idle}; handle_info({Port, {exit_status, 0}}, #os_proc{port = Port} = OsProc) -> couch_log:info("OS Process terminated normally", []), diff --git a/src/couch/src/couch_primary_sup.erl b/src/couch/src/couch_primary_sup.erl index 32ee282d68..93a1d2112f 100644 --- a/src/couch/src/couch_primary_sup.erl +++ b/src/couch/src/couch_primary_sup.erl @@ -18,6 +18,7 @@ start_link() -> supervisor:start_link({local, couch_primary_services}, ?MODULE, []). init([]) -> + ok = couch_bt_engine_cache:create_tables(), Children = [ {couch_task_status, {couch_task_status, start_link, []}, permanent, brutal_kill, worker, @@ -45,7 +46,7 @@ init([]) -> ] ]}, permanent, 5000, worker, [ets_lru]} - ] ++ couch_servers(), + ] ++ couch_bt_engine_cache:sup_children() ++ couch_servers(), {ok, {{one_for_one, 10, 3600}, Children}}. couch_servers() -> diff --git a/src/couch/src/couch_proc_manager.erl b/src/couch/src/couch_proc_manager.erl index d90463bf32..aa538e23e7 100644 --- a/src/couch/src/couch_proc_manager.erl +++ b/src/couch/src/couch_proc_manager.erl @@ -162,7 +162,7 @@ handle_call({get_proc, #client{} = Client}, From, State) -> {noreply, State}; handle_call({ret_proc, #proc{} = Proc}, From, State) -> #proc{client = Ref, pid = Pid} = Proc, - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), gen_server:reply(From, true), case ets:lookup(?PROCS, Pid) of [#proc{} = ProcInt] -> @@ -615,7 +615,7 @@ make_proc(Pid, Lang, Mod) when is_binary(Lang) -> {ok, Proc}. assign_proc(Pid, #proc{client = undefined} = Proc0) when is_pid(Pid) -> - Proc = Proc0#proc{client = erlang:monitor(process, Pid)}, + Proc = Proc0#proc{client = monitor(process, Pid)}, % It's important to insert the proc here instead of doing an update_element % as we might have updated the db_key or ddoc_keys in teach_ddoc/4 ets:insert(?PROCS, Proc), @@ -730,13 +730,25 @@ remove_waiting_client(#client{wait_key = Key}) -> ets:delete(?WAITERS, Key). get_proc_config() -> - Limit = config:get_boolean("query_server_config", "reduce_limit", true), - Timeout = get_os_process_timeout(), {[ - {<<"reduce_limit">>, Limit}, - {<<"timeout">>, Timeout} + {<<"reduce_limit">>, get_reduce_limit()}, + {<<"reduce_limit_threshold">>, couch_query_servers:reduce_limit_threshold()}, + {<<"reduce_limit_ratio">>, couch_query_servers:reduce_limit_ratio()}, + {<<"timeout">>, get_os_process_timeout()} ]}. +% Reduce limit is a tri-state value of true, false or log. The default value if +% is true. That's also the value if anything other than those 3 values are +% specified. +% +get_reduce_limit() -> + case config:get("query_server_config", "reduce_limit", "true") of + "false" -> false; + "log" -> log; + "true" -> true; + Other when is_list(Other) -> true + end. + get_hard_limit() -> config:get_integer("query_server_config", "os_process_limit", 100). diff --git a/src/couch/src/couch_query_servers.erl b/src/couch/src/couch_query_servers.erl index 3b222e0810..a204a767f9 100644 --- a/src/couch/src/couch_query_servers.erl +++ b/src/couch/src/couch_query_servers.erl @@ -19,6 +19,7 @@ -export([filter_view/4]). -export([finalize/2]). -export([rewrite/3]). +-export([reduce_limit_threshold/0, reduce_limit_ratio/0]). -export([with_ddoc_proc/3, proc_prompt/2, ddoc_prompt/4, ddoc_proc_prompt/3, json_doc/1]). @@ -278,7 +279,7 @@ sum_arrays(Else, _) -> throw_sum_error(Else). check_sum_overflow(InSize, OutSize, Sum) -> - Overflowed = OutSize > 4906 andalso OutSize * 2 > InSize, + Overflowed = OutSize > reduce_limit_threshold() andalso OutSize * reduce_limit_ratio() > InSize, case config:get("query_server_config", "reduce_limit", "true") of "true" when Overflowed -> Msg = log_sum_overflow(InSize, OutSize), @@ -302,6 +303,12 @@ log_sum_overflow(InSize, OutSize) -> couch_log:error(Msg, []), Msg. +reduce_limit_threshold() -> + config:get_integer("query_server_config", "reduce_limit_threshold", 5000). + +reduce_limit_ratio() -> + config:get_float("query_server_config", "reduce_limit_ratio", 2.0). + builtin_stats(_, []) -> {0, 0, 0, 0, 0}; builtin_stats(_, [[_, First] | Rest]) -> @@ -327,8 +334,8 @@ stat_values(Value, Acc) when is_tuple(Value), is_tuple(Acc) -> { Sum0 + Sum1, Cnt0 + Cnt1, - erlang:min(Min0, Min1), - erlang:max(Max0, Max1), + min(Min0, Min1), + max(Max0, Max1), Sqr0 + Sqr1 }; stat_values(Else, _Acc) -> diff --git a/src/couch/src/couch_server.erl b/src/couch/src/couch_server.erl index ca12a56fa9..246cdd1cba 100644 --- a/src/couch/src/couch_server.erl +++ b/src/couch/src/couch_server.erl @@ -303,7 +303,12 @@ init([N]) -> "couchdb", "update_lru_on_read", false ), ok = config:listen_for_changes(?MODULE, N), - ok = couch_file:init_delete_dir(RootDir), + % Spawn async .deleted files recursive cleaner, but only + % for the first sharded couch_server instance + case N of + 1 -> ok = couch_file:init_delete_dir(RootDir); + _ -> ok + end, ets:new(couch_dbs(N), [ set, protected, @@ -400,7 +405,7 @@ handle_config_terminate(_Server, _Reason, N) -> erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), {restart_config_listener, N}). per_couch_server(X) -> - erlang:max(1, X div num_servers()). + max(1, X div num_servers()). all_databases() -> {ok, DbList} = all_databases( @@ -858,7 +863,7 @@ get_engine(Server, DbName) -> [Engine] -> {ok, Engine}; _ -> - erlang:error(engine_conflict) + error(engine_conflict) end. get_possible_engines(DbName, RootDir, Engines) -> diff --git a/src/couch/src/couch_stream.erl b/src/couch/src/couch_stream.erl index ea375101fb..2998cd3ffa 100644 --- a/src/couch/src/couch_stream.erl +++ b/src/couch/src/couch_stream.erl @@ -185,7 +185,7 @@ init({Engine, OpenerPid, OpenerPriority, Options}) -> end, {ok, #stream{ engine = Engine, - opener_monitor = erlang:monitor(process, OpenerPid), + opener_monitor = monitor(process, OpenerPid), md5 = couch_hash:md5_hash_init(), identity_md5 = couch_hash:md5_hash_init(), encoding_fun = EncodingFun, @@ -269,7 +269,7 @@ handle_call(close, _From, Stream) -> StreamLen = WrittenLen + iolist_size(WriteBin2), {do_finalize(NewEngine), StreamLen, IdenLen, Md5Final, IdenMd5Final} end, - erlang:demonitor(MonRef), + demonitor(MonRef), {stop, normal, Result, Stream}. handle_cast(_Msg, State) -> diff --git a/src/couch/src/couch_task_status.erl b/src/couch/src/couch_task_status.erl index 29ab73692d..8577c304d5 100644 --- a/src/couch/src/couch_task_status.erl +++ b/src/couch/src/couch_task_status.erl @@ -107,7 +107,7 @@ handle_call({add_task, TaskProps}, {From, _}, Server) -> case ets:lookup(?MODULE, From) of [] -> true = ets:insert(?MODULE, {From, TaskProps}), - erlang:monitor(process, From), + monitor(process, From), {reply, ok, Server}; [_] -> {reply, {add_task_error, already_registered}, Server} @@ -130,7 +130,7 @@ handle_cast({update_status, Pid, NewProps}, Server) -> {noreply, Server}. handle_info({'DOWN', _MonitorRef, _Type, Pid, _Info}, Server) -> - %% should we also erlang:demonitor(_MonitorRef), ? + %% should we also demonitor(_MonitorRef), ? ets:delete(?MODULE, Pid), {noreply, Server}. diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl index bdd5c14f3b..e92caab93a 100644 --- a/src/couch/src/couch_util.erl +++ b/src/couch/src/couch_util.erl @@ -137,7 +137,7 @@ to_existing_atom(V) when is_list(V) -> end; to_existing_atom(V) when is_binary(V) -> try - list_to_existing_atom(?b2l(V)) + binary_to_existing_atom(V) catch _:_ -> V end; @@ -147,7 +147,7 @@ to_existing_atom(V) when is_atom(V) -> shutdown_sync(Pid) when not is_pid(Pid) -> ok; shutdown_sync(Pid) -> - MRef = erlang:monitor(process, Pid), + MRef = monitor(process, Pid), try catch unlink(Pid), catch exit(Pid, shutdown), @@ -156,7 +156,7 @@ shutdown_sync(Pid) -> ok end after - erlang:demonitor(MRef, [flush]) + demonitor(MRef, [flush]) end. validate_utf8(Data) when is_list(Data) -> @@ -208,31 +208,8 @@ to_hex(Binary) when is_binary(Binary) -> to_hex(List) when is_list(List) -> binary_to_list(to_hex_bin(list_to_binary(List))). -% Optimized encode_hex/1 function from Erlang/OTP binary module starting with OTP 24+ [1]. -% One exception is we are emitting lower case hex characters instead of upper case ones. -% -% [1] https://github.com/erlang/otp/blob/master/lib/stdlib/src/binary.erl#L365. -% - --define(HEX(X), (hex(X)):16). - -%% erlfmt-ignore -to_hex_bin(Data) when byte_size(Data) rem 8 =:= 0 -> - << <> || <> <= Data >>; -to_hex_bin(Data) when byte_size(Data) rem 7 =:= 0 -> - << <> || <> <= Data >>; -to_hex_bin(Data) when byte_size(Data) rem 6 =:= 0 -> - << <> || <> <= Data >>; -to_hex_bin(Data) when byte_size(Data) rem 5 =:= 0 -> - << <> || <> <= Data >>; -to_hex_bin(Data) when byte_size(Data) rem 4 =:= 0 -> - << <> || <> <= Data >>; -to_hex_bin(Data) when byte_size(Data) rem 3 =:= 0 -> - << <> || <> <= Data >>; -to_hex_bin(Data) when byte_size(Data) rem 2 =:= 0 -> - << <> || <> <= Data >>; to_hex_bin(Data) when is_binary(Data) -> - << <> || <> <= Data >>. + binary:encode_hex(Data, lowercase). parse_term(Bin) when is_binary(Bin) -> parse_term(binary_to_list(Bin)); @@ -251,8 +228,14 @@ get_value(Key, List, Default) -> Default end. -set_value(Key, List, Value) -> - lists:keyreplace(Key, 1, List, {Key, Value}). +% insert or update a {Key, Value} tuple in a list of tuples +-spec set_value(Key, TupleList1, Value) -> TupleList2 when + Key :: term(), + TupleList1 :: [tuple()], + Value :: term(), + TupleList2 :: [tuple()]. +set_value(Key, TupleList1, Value) -> + lists:keystore(Key, 1, TupleList1, {Key, Value}). get_nested_json_value({Props}, [Key | Keys]) -> case couch_util:get_value(Key, Props, nil) of @@ -446,16 +429,16 @@ to_binary(V) when is_list(V) -> list_to_binary(io_lib:format("~p", [V])) end; to_binary(V) when is_atom(V) -> - list_to_binary(atom_to_list(V)); + atom_to_binary(V); to_binary(V) -> list_to_binary(io_lib:format("~p", [V])). to_integer(V) when is_integer(V) -> V; to_integer(V) when is_list(V) -> - erlang:list_to_integer(V); + list_to_integer(V); to_integer(V) when is_binary(V) -> - erlang:list_to_integer(binary_to_list(V)). + binary_to_integer(V). to_list(V) when is_list(V) -> V; @@ -528,13 +511,15 @@ reorder_results(Keys, SortedResults, Default) -> Map = maps:from_list(SortedResults), [maps:get(Key, Map, Default) || Key <- Keys]. -url_strip_password(Url) -> +url_strip_password(Url) when is_list(Url) -> re:replace( Url, "(http|https|socks5)://([^:]+):[^@]+@(.*)$", "\\1://\\2:*****@\\3", [{return, list}] - ). + ); +url_strip_password(Other) -> + Other. encode_doc_id(#doc{id = Id}) -> encode_doc_id(Id); @@ -568,7 +553,7 @@ with_db(Db, Fun) -> true -> Fun(Db); false -> - erlang:error({invalid_db, Db}) + error({invalid_db, Db}) end. rfc1123_date() -> @@ -645,7 +630,7 @@ find_in_binary(_B, <<>>) -> find_in_binary(B, Data) -> case binary:match(Data, [B], []) of nomatch -> - MatchLength = erlang:min(byte_size(B), byte_size(Data)), + MatchLength = min(byte_size(B), byte_size(Data)), match_prefix_at_end( binary:part(B, {0, MatchLength}), binary:part(Data, {byte_size(Data), -MatchLength}), @@ -739,7 +724,7 @@ ensure_loaded(_Module) -> %% a function that does a receive as it would hijack incoming messages. with_proc(M, F, A, Timeout) -> {Pid, Ref} = spawn_monitor(fun() -> - exit({reply, erlang:apply(M, F, A)}) + exit({reply, apply(M, F, A)}) end), receive {'DOWN', Ref, process, Pid, {reply, Resp}} -> @@ -747,7 +732,7 @@ with_proc(M, F, A, Timeout) -> {'DOWN', Ref, process, Pid, Error} -> {error, Error} after Timeout -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), {error, timeout} end. @@ -787,39 +772,9 @@ version_to_binary(Ver) when is_tuple(Ver) -> version_to_binary(Ver) when is_list(Ver) -> IsZero = fun(N) -> N == 0 end, Ver1 = lists:reverse(lists:dropwhile(IsZero, lists:reverse(Ver))), - Ver2 = [erlang:integer_to_list(N) || N <- Ver1], + Ver2 = [integer_to_list(N) || N <- Ver1], ?l2b(lists:join(".", Ver2)). --compile({inline, [hex/1]}). - -%% erlfmt-ignore -hex(X) -> - % We are encoding hex directly as lower case ASCII here and it's just a lookup table - % for all the 00..ff combinations. 0x00 -> "00", 0xf1-> "f1", etc. - % - % 00, ..., 0f, - % .. .. - % f0, ..., ff - % - element(X + 1, { - 16#3030, 16#3031, 16#3032, 16#3033, 16#3034, 16#3035, 16#3036, 16#3037, 16#3038, 16#3039, 16#3061, 16#3062, 16#3063, 16#3064, 16#3065, 16#3066, - 16#3130, 16#3131, 16#3132, 16#3133, 16#3134, 16#3135, 16#3136, 16#3137, 16#3138, 16#3139, 16#3161, 16#3162, 16#3163, 16#3164, 16#3165, 16#3166, - 16#3230, 16#3231, 16#3232, 16#3233, 16#3234, 16#3235, 16#3236, 16#3237, 16#3238, 16#3239, 16#3261, 16#3262, 16#3263, 16#3264, 16#3265, 16#3266, - 16#3330, 16#3331, 16#3332, 16#3333, 16#3334, 16#3335, 16#3336, 16#3337, 16#3338, 16#3339, 16#3361, 16#3362, 16#3363, 16#3364, 16#3365, 16#3366, - 16#3430, 16#3431, 16#3432, 16#3433, 16#3434, 16#3435, 16#3436, 16#3437, 16#3438, 16#3439, 16#3461, 16#3462, 16#3463, 16#3464, 16#3465, 16#3466, - 16#3530, 16#3531, 16#3532, 16#3533, 16#3534, 16#3535, 16#3536, 16#3537, 16#3538, 16#3539, 16#3561, 16#3562, 16#3563, 16#3564, 16#3565, 16#3566, - 16#3630, 16#3631, 16#3632, 16#3633, 16#3634, 16#3635, 16#3636, 16#3637, 16#3638, 16#3639, 16#3661, 16#3662, 16#3663, 16#3664, 16#3665, 16#3666, - 16#3730, 16#3731, 16#3732, 16#3733, 16#3734, 16#3735, 16#3736, 16#3737, 16#3738, 16#3739, 16#3761, 16#3762, 16#3763, 16#3764, 16#3765, 16#3766, - 16#3830, 16#3831, 16#3832, 16#3833, 16#3834, 16#3835, 16#3836, 16#3837, 16#3838, 16#3839, 16#3861, 16#3862, 16#3863, 16#3864, 16#3865, 16#3866, - 16#3930, 16#3931, 16#3932, 16#3933, 16#3934, 16#3935, 16#3936, 16#3937, 16#3938, 16#3939, 16#3961, 16#3962, 16#3963, 16#3964, 16#3965, 16#3966, - 16#6130, 16#6131, 16#6132, 16#6133, 16#6134, 16#6135, 16#6136, 16#6137, 16#6138, 16#6139, 16#6161, 16#6162, 16#6163, 16#6164, 16#6165, 16#6166, - 16#6230, 16#6231, 16#6232, 16#6233, 16#6234, 16#6235, 16#6236, 16#6237, 16#6238, 16#6239, 16#6261, 16#6262, 16#6263, 16#6264, 16#6265, 16#6266, - 16#6330, 16#6331, 16#6332, 16#6333, 16#6334, 16#6335, 16#6336, 16#6337, 16#6338, 16#6339, 16#6361, 16#6362, 16#6363, 16#6364, 16#6365, 16#6366, - 16#6430, 16#6431, 16#6432, 16#6433, 16#6434, 16#6435, 16#6436, 16#6437, 16#6438, 16#6439, 16#6461, 16#6462, 16#6463, 16#6464, 16#6465, 16#6466, - 16#6530, 16#6531, 16#6532, 16#6533, 16#6534, 16#6535, 16#6536, 16#6537, 16#6538, 16#6539, 16#6561, 16#6562, 16#6563, 16#6564, 16#6565, 16#6566, - 16#6630, 16#6631, 16#6632, 16#6633, 16#6634, 16#6635, 16#6636, 16#6637, 16#6638, 16#6639, 16#6661, 16#6662, 16#6663, 16#6664, 16#6665, 16#6666 - }). - verify_hash_names(HashAlgorithms, SupportedHashes) -> verify_hash_names(HashAlgorithms, SupportedHashes, []). verify_hash_names([], _, HashNames) -> @@ -856,11 +811,11 @@ remove_sensitive_data(KVList) -> lists:keyreplace(password, 1, KVList1, {password, <<"****">>}). ejson_to_map(#{} = Val) -> - maps:map(fun(_, V) -> ejson_to_map(V) end, Val); + #{K => ejson_to_map(V) || K := V <- Val}; ejson_to_map(Val) when is_list(Val) -> - lists:map(fun(V) -> ejson_to_map(V) end, Val); + [ejson_to_map(V) || V <- Val]; ejson_to_map({Val}) when is_list(Val) -> - maps:from_list(lists:map(fun({K, V}) -> {K, ejson_to_map(V)} end, Val)); + #{K => ejson_to_map(V) || {K, V} <- Val}; ejson_to_map(Val) -> Val. diff --git a/src/couch/src/couch_uuids.erl b/src/couch/src/couch_uuids.erl index 588136159c..f007eab91c 100644 --- a/src/couch/src/couch_uuids.erl +++ b/src/couch/src/couch_uuids.erl @@ -17,7 +17,7 @@ -export([start/0, stop/0]). -export([new/0, random/0]). - +-export([v7_hex/0, v7_bin/0]). -export([init/1]). -export([handle_call/3, handle_cast/2, handle_info/2]). @@ -44,6 +44,8 @@ init([]) -> handle_call(create, _From, random) -> {reply, random(), random}; +handle_call(create, _From, uuid_v7) -> + {reply, v7_hex(), uuid_v7}; handle_call(create, _From, {utc_random, ClockSeq}) -> {UtcRandom, NewClockSeq} = utc_random(ClockSeq), {reply, UtcRandom, {utc_random, NewClockSeq}}; @@ -84,6 +86,32 @@ handle_config_terminate(_Server, _Reason, _State) -> gen_server:cast(?MODULE, change), erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener). +%% UUID Version 7 +%% https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7 +%% +%% 0 1 2 3 +%% 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +%% +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +%% | unix_ts_ms | +%% +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +%% | unix_ts_ms | ver | rand_a | +%% +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +%% |var| rand_b | +%% +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +%% | rand_b | +%% +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +%% +%% ver = 0111 = 7 +%% var = 10 = 2 +%% +v7_bin() -> + MSec = os:system_time(millisecond), + <> = crypto:strong_rand_bytes(10), + <>. + +v7_hex() -> + couch_util:to_hex_bin(v7_bin()). + new_prefix() -> couch_util:to_hex((crypto:strong_rand_bytes(13))). @@ -104,6 +132,8 @@ state() -> {utc_id, UtcIdSuffix, ClockSeq}; sequential -> {sequential, new_prefix(), inc()}; + uuid_v7 -> + uuid_v7; Unknown -> throw({unknown_uuid_algorithm, Unknown}) end. diff --git a/src/couch/src/test_util.erl b/src/couch/src/test_util.erl index 430c54788a..b8001840fb 100644 --- a/src/couch/src/test_util.erl +++ b/src/couch/src/test_util.erl @@ -155,7 +155,7 @@ stop_sync(Name, Reason, Timeout) when is_atom(Name) -> stop_sync(Pid, Reason, Timeout) when is_atom(Reason) and is_pid(Pid) -> stop_sync(Pid, fun() -> exit(Pid, Reason) end, Timeout); stop_sync(Pid, Fun, Timeout) when is_function(Fun) and is_pid(Pid) -> - MRef = erlang:monitor(process, Pid), + MRef = monitor(process, Pid), try begin catch unlink(Pid), @@ -168,7 +168,7 @@ stop_sync(Pid, Fun, Timeout) when is_function(Fun) and is_pid(Pid) -> end end after - erlang:demonitor(MRef, [flush]) + demonitor(MRef, [flush]) end; stop_sync(_, _, _) -> error(badarg). @@ -380,7 +380,7 @@ load_applications_with_stats() -> stats_file_to_app(File) -> [_Desc, _Priv, App | _] = lists:reverse(filename:split(File)), - erlang:list_to_atom(App). + list_to_atom(App). calculate_start_order(Apps) -> AllApps = calculate_start_order(sort_apps(Apps), []), diff --git a/src/couch/test/eunit/couch_bt_engine_cache_test.erl b/src/couch/test/eunit/couch_bt_engine_cache_test.erl new file mode 100644 index 0000000000..0eade12a41 --- /dev/null +++ b/src/couch/test/eunit/couch_bt_engine_cache_test.erl @@ -0,0 +1,102 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_bt_engine_cache_test). + +-include_lib("couch/include/couch_eunit.hrl"). + +couch_bt_engine_cache_test_() -> + { + foreach, + fun setup/0, + fun teardown/1, + [ + ?TDEF_FE(t_created), + ?TDEF_FE(t_insert_and_lookup), + ?TDEF_FE(t_decay_and_removal_works, 10), + ?TDEF_FE(t_pid_cleanup_works, 10) + ] + }. + +setup() -> + Ctx = test_util:start_applications([config]), + couch_bt_engine_cache:create_tables(), + #{shard_count := N} = couch_bt_engine_cache:info(), + Pids = lists:map( + fun(I) -> + {ok, Pid} = couch_bt_engine_cache:start_link(I), + Pid + end, + lists:seq(1, N) + ), + {Ctx, Pids}. + +teardown({Ctx, [_ | _] = Pids}) -> + lists:foreach( + fun(Pid) -> + catch unlink(Pid), + exit(Pid, kill) + end, + Pids + ), + clear_tables(), + config:delete("bt_engine_cache", "max_size", false), + config:delete("bt_engine_cache", "leave_percent", false), + test_util:stop_applications(Ctx). + +clear_tables() -> + lists:foreach(fun ets:delete/1, couch_bt_engine_cache:tables()). + +t_created(_) -> + Info = couch_bt_engine_cache:info(), + #{size := Size, memory := Mem, shard_count := N} = Info, + ?assert(N >= 16, "shard count is greater or equal to 16"), + ?assertEqual(0, Size), + ?assert(is_integer(Mem)), + ?assert(Mem >= 0). + +t_insert_and_lookup(_) -> + ?assertError(function_clause, couch_bt_engine_cache:insert(not_a_pid, 1, foo)), + ?assertError(function_clause, couch_bt_engine_cache:insert(self(), xyz, foo)), + ?assertMatch(#{size := 0}, couch_bt_engine_cache:info()), + ?assert(couch_bt_engine_cache:insert({pid, 42}, term)), + ?assertMatch(#{size := 1}, couch_bt_engine_cache:info()), + ?assertNot(couch_bt_engine_cache:insert({pid, 42}, term)), + ?assertEqual(term, couch_bt_engine_cache:lookup({pid, 42})), + ?assertEqual(undefined, couch_bt_engine_cache:lookup({pid, 43})). + +t_decay_and_removal_works(_) -> + config:set("bt_engine_cache", "leave_percent", "0", false), + Term = [foo, bar, baz, lists:seq(1, 100)], + [couch_bt_engine_cache:insert({pid, I}, Term) || I <- lists:seq(1, 10000)], + WaitFun = fun() -> + #{size := Size} = couch_bt_engine_cache:info(), + case Size > 0 of + true -> wait; + false -> ok + end + end, + test_util:wait(WaitFun, 7500), + ?assertMatch(#{size := 0}, couch_bt_engine_cache:info()). + +t_pid_cleanup_works(_) -> + Pid = spawn(fun() -> timer:sleep(2000) end), + [couch_bt_engine_cache:insert({Pid, I}, baz) || I <- lists:seq(1, 1000)], + WaitFun = fun() -> + #{size := Size} = couch_bt_engine_cache:info(), + case Size > 0 of + true -> wait; + false -> ok + end + end, + test_util:wait(WaitFun, 7500), + ?assertMatch(#{size := 0}, couch_bt_engine_cache:info()). diff --git a/src/couch/test/eunit/couch_bt_engine_compactor_ev_tests.erl b/src/couch/test/eunit/couch_bt_engine_compactor_ev_tests.erl index 007c74d06e..803d6295f2 100644 --- a/src/couch/test/eunit/couch_bt_engine_compactor_ev_tests.erl +++ b/src/couch/test/eunit/couch_bt_engine_compactor_ev_tests.erl @@ -264,7 +264,7 @@ run_successful_compaction(DbName) -> {ok, ContinueFun} = ?EV_MOD:set_wait(init), {ok, Db} = couch_db:open_int(DbName, []), {ok, CPid} = couch_db:start_compact(Db), - Ref = erlang:monitor(process, CPid), + Ref = monitor(process, CPid), ContinueFun(CPid), receive {'DOWN', Ref, _, _, normal} -> ok @@ -278,7 +278,7 @@ wait_db_cleared(Db) -> wait_db_cleared(Db, 5). wait_db_cleared(Db, N) when N < 0 -> - erlang:error({db_clear_timeout, couch_db:name(Db)}); + error({db_clear_timeout, couch_db:name(Db)}); wait_db_cleared(Db, N) -> Tab = couch_server:couch_dbs(couch_db:name(Db)), case ets:lookup(Tab, couch_db:name(Db)) of @@ -299,7 +299,7 @@ wait_db_cleared(Db, N) -> populate_db(_Db, NumDocs) when NumDocs =< 0 -> ok; populate_db(Db, NumDocs) -> - String = [$a || _ <- lists:seq(1, erlang:min(NumDocs, 500))], + String = [$a || _ <- lists:seq(1, min(NumDocs, 500))], Docs = lists:map( fun(_) -> couch_doc:from_json_obj( diff --git a/src/couch/test/eunit/couch_bt_engine_compactor_tests.erl b/src/couch/test/eunit/couch_bt_engine_compactor_tests.erl index 4ed57668ef..a7fec17db2 100644 --- a/src/couch/test/eunit/couch_bt_engine_compactor_tests.erl +++ b/src/couch/test/eunit/couch_bt_engine_compactor_tests.erl @@ -89,7 +89,7 @@ check_db_validity(DbName) -> with_mecked_emsort(Fun) -> meck:new(couch_emsort, [passthrough]), - meck:expect(couch_emsort, iter, fun(_) -> erlang:error(kaboom) end), + meck:expect(couch_emsort, iter, fun(_) -> error(kaboom) end), try Fun() after @@ -131,7 +131,7 @@ wait_db_compact_done(_DbName, 0) -> {line, ?LINE}, {reason, "DB compaction failed to finish"} ], - erlang:error({assertion_failed, Failure}); + error({assertion_failed, Failure}); wait_db_compact_done(DbName, N) -> IsDone = couch_util:with_db(DbName, fun(Db) -> not is_pid(couch_db:get_compactor_pid(Db)) diff --git a/src/couch/test/eunit/couch_btree_prop_tests.erl b/src/couch/test/eunit/couch_btree_prop_tests.erl new file mode 100644 index 0000000000..1a9bd4f753 --- /dev/null +++ b/src/couch/test/eunit/couch_btree_prop_tests.erl @@ -0,0 +1,225 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_btree_prop_tests). + +-ifdef(WITH_PROPER). + +-export([ + command/1, + initial_state/0, + next_state/3, + precondition/2, + postcondition/3, + query_modify/3, + lookup/1 +]). + +% Process dict keys +-define(BTREE, btree). +-define(BTREE_FILENAME, btree_filename). + +-include_lib("couch/include/couch_eunit_proper.hrl"). +-include_lib("couch/include/couch_eunit.hrl"). + +btree_property_test_() -> + ?EUNIT_QUICKCHECK(90, 3000). + +% +% Properties +% + +prop_btree_() -> + ?FORALL( + Cmds, + commands(?MODULE), + begin + setup_btree(), + {Hist, St, Res} = run_commands(?MODULE, Cmds), + cleanup_btree(), + ?WHENFAIL( + on_failure(Hist, St, Res), + aggregate(command_names(Cmds), Res =:= ok) + ) + end + ). + +% +% Setup, teardown and proxy calls to couch_btree. +% + +% PropEr is a bit awkward when it comes to test setup and teardown, so to make +% everything easier use plain function calls in the test and keep the btree +% state in the process dictionary. + +setup_btree() -> + Filename = ?tempfile(), + {ok, Fd} = couch_file:open(Filename, [create, overwrite]), + {ok, Bt} = couch_btree:open(nil, Fd, [{compression, none}]), + put(?BTREE, Bt), + put(?BTREE_FILENAME, Filename). + +cleanup_btree() -> + Bt = get(?BTREE), + Fd = couch_btree:get_fd(Bt), + ok = couch_file:close(Fd), + ok = file:delete(get(?BTREE_FILENAME)), + erase(?BTREE), + erase(?BTREE_FILENAME). + +get_btree() -> + Bt = get(?BTREE), + % Sanity check. The btree didn't somehow disappear. + true = couch_btree:is_btree(Bt), + Bt. + +update_btree(NewBt) -> + OldBt = get(?BTREE), + % Sanity check. Expect an old btree instance to exist. + true = couch_btree:is_btree(OldBt), + true = couch_btree:is_btree(NewBt), + put(?BTREE, NewBt), + ok. + +% add/2 and add_remove/3 call query_modify/4, so just test that +% +query_modify(Lookups, Inserts, Removes) -> + Bt = get_btree(), + {ok, QueryResults, Bt1} = couch_btree:query_modify(Bt, Lookups, Inserts, Removes), + ok = update_btree(Bt1), + {ok, QueryResults, Bt1}. + +lookup(Keys) -> + Bt = get_btree(), + couch_btree:lookup(Bt, Keys). + +foldl() -> + Fun = fun({K, V}, Acc) -> {ok, [{K, V} | Acc]} end, + {ok, _, Acc1} = couch_btree:foldl(get_btree(), Fun, []), + lists:reverse(Acc1). + +% +% PropEr state callbacks +% + +% Model the btree as a simple orddict. +% +initial_state() -> + orddict:new(). + +command(Model) when is_list(Model) -> + frequency([ + {1, {call, ?MODULE, query_modify, [keys(), kvs(), remove_keys()]}}, + {1, {call, ?MODULE, lookup, [keys()]}} + ]). + +% These are called during command generation, the test with an actual model and +% during shrinking to guide the shrinking behavior. +% +precondition(_Model, {call, ?MODULE, query_modify, [_, _, _]}) -> + true; +precondition(Model, {call, ?MODULE, lookup, [Keys]}) -> + % Avoid exploring too many useless cases and only look up keys if we know + % we have some keys to look at + orddict:size(Model) > 0 andalso length(Keys) > 0. + +% Assuming the postcondition passed, advance to the next state +% +next_state(Model, _, {call, ?MODULE, query_modify, [_Lookups, Inserts, Removes]}) -> + model_add_remove(Model, Inserts, Removes); +next_state(Model, _, {call, ?MODULE, lookup, [_]}) -> + Model. + +% The Model is *before* the call is applied. The Result is *after* the command +% was applied to the actual btree we're testing. This is where the "model" vs +% "real" btree check happens. +% +postcondition(Model, {call, ?MODULE, query_modify, [Lookups, Inserts, Removes]}, Result) -> + {ok, QueryResults, _Bt} = Result, + ModelUpdate = model_add_remove(Model, Inserts, Removes), + ModelLookup = model_query(Model, Lookups), + ModelLookup == lists:sort(QueryResults) andalso ModelUpdate == foldl(); +postcondition(Model, {call, ?MODULE, lookup, [Lookups]}, Result) -> + model_lookup(Model, Lookups) == Result. + +% +% Generators +% + +key() -> + integer(1, 1000). + +val() -> + elements([a, b, c]). + +kvs() -> + list({key(), val()}). + +keys() -> + list(key()). + +remove_keys() -> + % Bias a bit towards not removing keys + frequency([{4, []}, {1, keys()}]). + +% +% Helper functions +% + +% Model (orddict) helpers + +model_add_remove(Model, Inserts, Removes) -> + % Keep this in sync with the op_order/1 from the + % couch_btree model: + % op_order(fetch) -> 1; + % op_order(remove) -> 2; + % op_order(insert) -> 3. + Model1 = model_remove(Model, Removes), + Model2 = model_insert(Model1, Inserts), + Model2. + +model_insert(Model, KVs) -> + UsortFun = fun({K1, _}, {K2, _}) -> K1 =< K2 end, + FoldFun = fun({K, V}, M) -> orddict:store(K, V, M) end, + lists:foldl(FoldFun, Model, lists:usort(UsortFun, KVs)). + +model_remove(Model, Keys) -> + FoldFun = fun(K, M) -> orddict:erase(K, M) end, + lists:foldl(FoldFun, Model, lists:usort(Keys)). + +model_lookup(Model, Keys) -> + Fun = + fun(K) -> + case orddict:find(K, Model) of + {ok, V} -> {ok, {K, V}}; + error -> not_found + end + end, + lists:map(Fun, Keys). + +model_query(Model, Keys) -> + Fun = + fun(K) -> + case orddict:find(K, Model) of + {ok, V} -> {ok, {K, V}}; + error -> {not_found, {K, nil}} + end + end, + lists:sort(lists:map(Fun, Keys)). + +% Other helpers + +on_failure(History, St, Res) -> + Msg = "~nWHENFAIL: History: ~p\nState: ~p\nResult: ~p\n", + io:format(standard_error, Msg, [History, St, Res]). + +-endif. diff --git a/src/couch/test/eunit/couch_btree_tests.erl b/src/couch/test/eunit/couch_btree_tests.erl index 1ec92818bb..f365a37cf1 100644 --- a/src/couch/test/eunit/couch_btree_tests.erl +++ b/src/couch/test/eunit/couch_btree_tests.erl @@ -13,7 +13,6 @@ -module(couch_btree_tests). -include_lib("couch/include/couch_eunit.hrl"). --include_lib("couch/include/couch_db.hrl"). -define(ROWS, 1000). % seconds @@ -30,6 +29,35 @@ setup() -> setup_kvs(_) -> setup(). +setup_kvs_with_cache(_) -> + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + {ok, Btree} = couch_btree:open(nil, Fd, [ + {compression, none}, + {reduce, fun reduce_fun/2}, + {cache_depth, 2} + ]), + {Fd, Btree}. + +setup_kvs_with_small_chunk_size(_) -> + % Less than a 1/4 than current default 1279 + config:set("couchdb", "btree_chunk_size", "300", false), + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + {ok, Btree} = couch_btree:open(nil, Fd, [ + {compression, none}, + {reduce, fun reduce_fun/2} + ]), + {Fd, Btree}. + +setup_kvs_with_large_chunk_size(_) -> + % About 4x than current default 1279 + config:set("couchdb", "btree_chunk_size", "5000", false), + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + {ok, Btree} = couch_btree:open(nil, Fd, [ + {compression, none}, + {reduce, fun reduce_fun/2} + ]), + {Fd, Btree}. + setup_red() -> {_, EvenOddKVs} = lists:foldl( fun(Idx, {Key, Acc}) -> @@ -48,6 +76,7 @@ setup_red(_) -> setup_red(). teardown(Fd) when is_pid(Fd) -> + config:delete("couchdb", "btree_chunk_size", false), ok = couch_file:close(Fd); teardown({Fd, _}) -> teardown(Fd). @@ -81,7 +110,7 @@ btree_open_test_() -> {ok, Btree} = couch_btree:open(nil, Fd, [{compression, none}]), { "Ensure that created btree is really a btree record", - ?_assert(is_record(Btree, btree)) + ?_assert(couch_btree:is_btree(Btree)) }. sorted_kvs_test_() -> @@ -105,7 +134,7 @@ sorted_kvs_test_() -> rsorted_kvs_test_() -> Sorted = [{Seq, rand:uniform()} || Seq <- lists:seq(1, ?ROWS)], Funs = kvs_test_funs(), - Reversed = Sorted, + Reversed = lists:reverse(Sorted), { "BTree with backward sorted keys", { @@ -140,6 +169,60 @@ shuffled_kvs_test_() -> } }. +sorted_kvs_with_cache_test_() -> + Funs = kvs_test_funs(), + Sorted = [{Seq, rand:uniform()} || Seq <- lists:seq(1, ?ROWS)], + { + "BTree with a cache and sorted keys", + { + setup, + fun() -> test_util:start_couch() end, + fun test_util:stop/1, + { + foreachx, + fun setup_kvs_with_cache/1, + fun teardown/2, + [{Sorted, Fun} || Fun <- Funs] + } + } + }. + +sorted_kvs_small_chunk_size_test_() -> + Funs = kvs_test_funs(), + Sorted = [{Seq, rand:uniform()} || Seq <- lists:seq(1, ?ROWS)], + { + "BTree with a small chunk size and sorted keys", + { + setup, + fun() -> test_util:start_couch() end, + fun test_util:stop/1, + { + foreachx, + fun setup_kvs_with_small_chunk_size/1, + fun teardown/2, + [{Sorted, Fun} || Fun <- Funs] + } + } + }. + +sorted_kvs_large_chunk_size_test_() -> + Funs = kvs_test_funs(), + Sorted = [{Seq, rand:uniform()} || Seq <- lists:seq(1, ?ROWS)], + { + "BTree with a large chunk size and sorted keys", + { + setup, + fun() -> test_util:start_couch() end, + fun test_util:stop/1, + { + foreachx, + fun setup_kvs_with_large_chunk_size/1, + fun teardown/2, + [{Sorted, Fun} || Fun <- Funs] + } + } + }. + reductions_test_() -> { "BTree reductions", @@ -189,10 +272,10 @@ reductions_test_() -> }. should_set_fd_correctly(_, {Fd, Btree}) -> - ?_assertMatch(Fd, Btree#btree.fd). + ?_assertMatch(Fd, couch_btree:get_fd(Btree)). should_set_root_correctly(_, {_, Btree}) -> - ?_assertMatch(nil, Btree#btree.root). + ?_assertMatch(nil, couch_btree:get_state(Btree)). should_create_zero_sized_btree(_, {_, Btree}) -> ?_assertMatch(0, couch_btree:size(Btree)). @@ -200,7 +283,7 @@ should_create_zero_sized_btree(_, {_, Btree}) -> should_set_reduce_option(_, {_, Btree}) -> ReduceFun = fun reduce_fun/2, Btree1 = couch_btree:set_options(Btree, [{reduce, ReduceFun}]), - ?_assertMatch(ReduceFun, Btree1#btree.reduce). + ?_assertMatch(ReduceFun, couch_btree:get_reduce_fun(Btree1)). should_fold_over_empty_btree(_, {_, Btree}) -> {ok, _, EmptyRes} = couch_btree:foldl(Btree, fun(_, X) -> {ok, X + 1} end, 0), @@ -228,7 +311,7 @@ should_have_lesser_size_than_file(Fd, Btree) -> should_keep_root_pointer_to_kp_node(Fd, Btree) -> ?_assertMatch( {ok, {kp_node, _}}, - couch_file:pread_term(Fd, element(1, Btree#btree.root)) + couch_file:pread_term(Fd, element(1, couch_btree:get_state(Btree))) ). should_remove_all_keys(KeyValues, Btree) -> @@ -615,18 +698,15 @@ test_key_access(Btree, List) -> FoldFun = fun(Element, {[HAcc | TAcc], Count}) -> case Element == HAcc of true -> {ok, {TAcc, Count + 1}}; - _ -> {ok, {TAcc, Count + 1}} + _ -> {ok, {TAcc, Count}} end end, Length = length(List), Sorted = lists:sort(List), {ok, _, {[], Length}} = couch_btree:foldl(Btree, FoldFun, {Sorted, 0}), - {ok, _, {[], Length}} = couch_btree:fold( - Btree, - FoldFun, - {Sorted, 0}, - [{dir, rev}] - ), + Reversed = lists:reverse(Sorted), + RevOpts = [{dir, rev}], + {ok, _, {[], Length}} = couch_btree:fold(Btree, FoldFun, {Reversed, 0}, RevOpts), ok. test_lookup_access(Btree, KeyValues) -> diff --git a/src/couch/test/eunit/couch_changes_tests.erl b/src/couch/test/eunit/couch_changes_tests.erl index bbeeac26ae..917689e860 100644 --- a/src/couch/test/eunit/couch_changes_tests.erl +++ b/src/couch/test/eunit/couch_changes_tests.erl @@ -1654,7 +1654,7 @@ wait_finished({_, ConsumerRef}) -> {'DOWN', ConsumerRef, _, _, Msg} when Msg == normal; Msg == ok -> ok; {'DOWN', ConsumerRef, _, _, Msg} -> - erlang:error( + error( {consumer_died, [ {module, ?MODULE}, {line, ?LINE}, @@ -1662,7 +1662,7 @@ wait_finished({_, ConsumerRef}) -> ]} ) after ?TIMEOUT -> - erlang:error( + error( {consumer_died, [ {module, ?MODULE}, {line, ?LINE}, @@ -1752,7 +1752,7 @@ maybe_pause(Parent, Acc) -> Parent ! {ok, Ref}, throw({stop, Acc}); V when V /= updated -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch/test/eunit/couch_file_tests.erl b/src/couch/test/eunit/couch_file_tests.erl index 5b74816360..c138a9c0ab 100644 --- a/src/couch/test/eunit/couch_file_tests.erl +++ b/src/couch/test/eunit/couch_file_tests.erl @@ -946,3 +946,86 @@ legacy_stats() -> reset_legacy_checksum_stats() -> Counter = couch_stats:sample([couchdb, legacy_checksums]), couch_stats:decrement_counter([couchdb, legacy_checksums], Counter). + +write_header_sync_test_() -> + { + "Test sync options for write_header", + { + setup, + fun test_util:start_couch/0, + fun test_util:stop_couch/1, + { + foreach, + fun unlinked_setup/0, + fun teardown/1, + [ + ?TDEF_FE(should_handle_sync_option), + ?TDEF_FE(should_not_sync_by_default), + ?TDEF_FE(should_handle_error_of_the_first_sync), + ?TDEF_FE(should_handle_error_of_the_second_sync), + ?TDEF_FE(should_handle_error_of_the_file_write) + ] + } + } + }. + +unlinked_setup() -> + Self = self(), + ReqId = make_ref(), + meck:new(file, [passthrough, unstick]), + spawn(fun() -> + {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), + Self ! {ReqId, Fd} + end), + receive + {ReqId, Result} -> Result + end. + +should_handle_sync_option(Fd) -> + ok = couch_file:write_header(Fd, {<<"some_data">>, 32}, [sync]), + ?assertMatch({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd)), + ?assertEqual(2, meck:num_calls(file, datasync, ['_'])), + ok. + +should_not_sync_by_default(Fd) -> + ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), + ?assertMatch({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd)), + ?assertEqual(0, meck:num_calls(file, datasync, ['_'])), + ok. + +should_handle_error_of_the_first_sync(Fd) -> + meck:expect( + file, + datasync, + ['_'], + meck:val({error, terminated}) + ), + ?assertEqual({error, terminated}, couch_file:write_header(Fd, {<<"some_data">>, 32}, [sync])), + ?assertEqual(1, meck:num_calls(file, datasync, ['_'])), + ok. + +should_handle_error_of_the_second_sync(Fd) -> + meck:expect( + file, + datasync, + ['_'], + meck:seq([ + meck:val(ok), + meck:val({error, terminated}) + ]) + ), + ?assertEqual({error, terminated}, couch_file:write_header(Fd, {<<"some_data">>, 32}, [sync])), + ?assertEqual(2, meck:num_calls(file, datasync, ['_'])), + ok. + +should_handle_error_of_the_file_write(Fd) -> + meck:expect( + file, + write, + ['_', '_'], + meck:val({error, terminated}) + ), + ?assertEqual({error, terminated}, couch_file:write_header(Fd, {<<"some_data">>, 32}, [sync])), + ?assertEqual(1, meck:num_calls(file, datasync, ['_'])), + ?assertEqual(1, meck:num_calls(file, write, ['_', '_'])), + ok. diff --git a/src/couch/test/eunit/couch_index_tests.erl b/src/couch/test/eunit/couch_index_tests.erl index 368f7a0596..3c078ca157 100644 --- a/src/couch/test/eunit/couch_index_tests.erl +++ b/src/couch/test/eunit/couch_index_tests.erl @@ -245,7 +245,7 @@ tracer_new() -> ok. tracer_delete() -> - dbg:stop_clear(), + dbg:stop(), (catch ets:delete(?MODULE)), ok. diff --git a/src/couch/test/eunit/couch_query_servers_tests.erl b/src/couch/test/eunit/couch_query_servers_tests.erl index 1ade40b670..a2fffb446d 100644 --- a/src/couch/test/eunit/couch_query_servers_tests.erl +++ b/src/couch/test/eunit/couch_query_servers_tests.erl @@ -16,96 +16,110 @@ -include_lib("couch/include/couch_eunit.hrl"). setup() -> - meck:new([config, couch_log]). + meck:new(couch_log, [passthrough]), + Ctx = test_util:start_couch([ioq]), + config:set("query_server_config", "reduce_limit", "true", false), + config:set("log", "level", "info", false), + Ctx. -teardown(_) -> +teardown(Ctx) -> + config:delete("query_server_config", "reduce_limit", true), + config:delete("query_server_config", "reduce_limit_threshold", true), + config:delete("query_server_config", "reduce_limit_ratio", true), + config:delete("log", "level", false), + test_util:stop_couch(Ctx), meck:unload(). -setup_oom() -> - test_util:start_couch([ioq]). - -teardown_oom(Ctx) -> - meck:unload(), - test_util:stop_couch(Ctx). - -sum_overflow_test_() -> +query_server_limits_test_() -> { - "Test overflow detection in the _sum reduce function", + "Test overflow detection and batch splitting in query server", { - setup, + foreach, fun setup/0, fun teardown/1, [ - fun should_return_error_on_overflow/0, - fun should_return_object_on_log/0, - fun should_return_object_on_false/0 - ] - } - }. - -filter_oom_test_() -> - { - "Test recovery from oom in filters", - { - setup, - fun setup_oom/0, - fun teardown_oom/1, - [ - fun should_split_large_batches/0 + ?TDEF_FE(builtin_should_return_error_on_overflow), + ?TDEF_FE(builtin_should_not_return_error_with_generous_overflow_threshold), + ?TDEF_FE(builtin_should_not_return_error_with_generous_overflow_ratio), + ?TDEF_FE(builtin_should_return_object_on_log), + ?TDEF_FE(builtin_should_return_object_on_false), + ?TDEF_FE(js_reduce_should_return_error_on_overflow), + ?TDEF_FE(js_reduce_should_return_object_on_log), + ?TDEF_FE(js_reduce_should_return_object_on_false), + ?TDEF_FE(should_split_large_batches) ] } }. -should_return_error_on_overflow() -> - meck:reset([config, couch_log]), - meck:expect( - config, - get, - ["query_server_config", "reduce_limit", "true"], - "true" - ), - meck:expect(couch_log, error, ['_', '_'], ok), +builtin_should_return_error_on_overflow(_) -> + config:set("query_server_config", "reduce_limit", "true", false), + meck:reset(couch_log), KVs = gen_sum_kvs(), {ok, [Result]} = couch_query_servers:reduce(<<"foo">>, [<<"_sum">>], KVs), ?assertMatch({[{<<"error">>, <<"builtin_reduce_error">>} | _]}, Result), - ?assert(meck:called(config, get, '_')), ?assert(meck:called(couch_log, error, '_')). -should_return_object_on_log() -> - meck:reset([config, couch_log]), - meck:expect( - config, - get, - ["query_server_config", "reduce_limit", "true"], - "log" - ), - meck:expect(couch_log, error, ['_', '_'], ok), +builtin_should_not_return_error_with_generous_overflow_threshold(_) -> + config:set("query_server_config", "reduce_limit", "true", false), + config:set_integer("query_server_config", "reduce_limit_threshold", 1000000, false), + meck:reset(couch_log), + KVs = gen_sum_kvs(), + {ok, [Result]} = couch_query_servers:reduce(<<"foo">>, [<<"_sum">>], KVs), + ?assertNotMatch({[{<<"error">>, <<"builtin_reduce_error">>} | _]}, Result). + +builtin_should_not_return_error_with_generous_overflow_ratio(_) -> + config:set("query_server_config", "reduce_limit", "true", false), + config:set_float("query_server_config", "reduce_limit_ratio", 0.1, false), + meck:reset(couch_log), + KVs = gen_sum_kvs(), + {ok, [Result]} = couch_query_servers:reduce(<<"foo">>, [<<"_sum">>], KVs), + ?assertNotMatch({[{<<"error">>, <<"builtin_reduce_error">>} | _]}, Result). + +builtin_should_return_object_on_log(_) -> + config:set("query_server_config", "reduce_limit", "log", false), + meck:reset(couch_log), KVs = gen_sum_kvs(), {ok, [Result]} = couch_query_servers:reduce(<<"foo">>, [<<"_sum">>], KVs), ?assertMatch({[_ | _]}, Result), Keys = [K || {K, _} <- element(1, Result)], ?assert(not lists:member(<<"error">>, Keys)), - ?assert(meck:called(config, get, '_')), ?assert(meck:called(couch_log, error, '_')). -should_return_object_on_false() -> - meck:reset([config, couch_log]), - meck:expect( - config, - get, - ["query_server_config", "reduce_limit", "true"], - "false" - ), - meck:expect(couch_log, error, ['_', '_'], ok), +builtin_should_return_object_on_false(_) -> + config:set("query_server_config", "reduce_limit", "false", false), + meck:reset(couch_log), KVs = gen_sum_kvs(), {ok, [Result]} = couch_query_servers:reduce(<<"foo">>, [<<"_sum">>], KVs), ?assertMatch({[_ | _]}, Result), Keys = [K || {K, _} <- element(1, Result)], ?assert(not lists:member(<<"error">>, Keys)), - ?assert(meck:called(config, get, '_')), ?assertNot(meck:called(couch_log, error, '_')). -should_split_large_batches() -> +js_reduce_should_return_error_on_overflow(_) -> + config:set("query_server_config", "reduce_limit", "true", false), + meck:reset(couch_log), + KVs = gen_sum_kvs(), + {ok, [Result]} = couch_query_servers:reduce(<<"javascript">>, [sum_js()], KVs), + ?assertMatch({[{reduce_overflow_error, <<"Reduce output must shrink", _/binary>>}]}, Result), + ?assert(meck:called(couch_log, error, '_')). + +js_reduce_should_return_object_on_log(_) -> + config:set("query_server_config", "reduce_limit", "log", false), + meck:reset(couch_log), + KVs = gen_sum_kvs(), + {ok, [Result]} = couch_query_servers:reduce(<<"javascript">>, [sum_js()], KVs), + ?assertMatch([<<"result">>, [_ | _]], Result), + ?assert(meck:called(couch_log, info, '_')). + +js_reduce_should_return_object_on_false(_) -> + config:set("query_server_config", "reduce_limit", "false", false), + meck:reset(couch_log), + KVs = gen_sum_kvs(), + {ok, [Result]} = couch_query_servers:reduce(<<"javascript">>, [sum_js()], KVs), + ?assertMatch([<<"result">>, [_ | _]], Result), + ?assertNot(meck:called(couch_log, info, '_')). + +should_split_large_batches(_) -> Req = {json_req, {[]}}, Db = <<"somedb">>, DDoc = #doc{ @@ -152,3 +166,13 @@ gen_sum_kvs() -> end, lists:seq(1, 10) ). + +sum_js() -> + % Don't do this in real views + << + "\n" + " function(keys, vals, rereduce) {\n" + " return ['result', vals.concat(vals)]\n" + " }\n" + " " + >>. diff --git a/src/couch/test/eunit/couch_server_tests.erl b/src/couch/test/eunit/couch_server_tests.erl index a43106d890..77ddfde213 100644 --- a/src/couch/test/eunit/couch_server_tests.erl +++ b/src/couch/test/eunit/couch_server_tests.erl @@ -224,7 +224,7 @@ t_interleaved_create_delete_open(DbName) -> % Now monitor and resume the couch_server and assert that % couch_server does not crash while processing OpenResultMsg - CSRef = erlang:monitor(process, CouchServer), + CSRef = monitor(process, CouchServer), erlang:resume_process(CouchServer), check_monitor_not_triggered(CSRef), @@ -253,7 +253,7 @@ get_opener_pid(DbName) -> wait_for_open_async_result(CouchServer, Opener) -> WaitFun = fun() -> - {_, Messages} = erlang:process_info(CouchServer, messages), + {_, Messages} = process_info(CouchServer, messages), Found = lists:foldl( fun(Msg, Acc) -> case Msg of @@ -276,7 +276,7 @@ wait_for_open_async_result(CouchServer, Opener) -> check_monitor_not_triggered(Ref) -> receive {'DOWN', Ref, _, _, Reason0} -> - erlang:error({monitor_triggered, Reason0}) + error({monitor_triggered, Reason0}) after 100 -> ok end. @@ -286,5 +286,5 @@ get_next_message() -> Msg -> Msg after 5000 -> - erlang:error(timeout) + error(timeout) end. diff --git a/src/couch/test/eunit/couch_stream_tests.erl b/src/couch/test/eunit/couch_stream_tests.erl index f29c04c82c..6bc2c7952c 100644 --- a/src/couch/test/eunit/couch_stream_tests.erl +++ b/src/couch/test/eunit/couch_stream_tests.erl @@ -124,7 +124,7 @@ should_stop_on_normal_exit_of_stream_opener({Fd, _}) -> end, ?assertNot(is_process_alive(OpenerPid)), % Verify the stream itself has also died - StreamRef = erlang:monitor(process, StreamPid), + StreamRef = monitor(process, StreamPid), receive {'DOWN', StreamRef, _, _, _} -> ok end, diff --git a/src/couch/test/eunit/couch_task_status_tests.erl b/src/couch/test/eunit/couch_task_status_tests.erl index e5d8ead980..90e25362ac 100644 --- a/src/couch/test/eunit/couch_task_status_tests.erl +++ b/src/couch/test/eunit/couch_task_status_tests.erl @@ -181,7 +181,7 @@ loop() -> end. call(Pid, done) -> - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), Pid ! {done, self()}, Res = wait(Pid), receive @@ -222,7 +222,7 @@ get_task_prop(Pid, Prop) -> ), case couch_util:get_value(Prop, hd(Element), nil) of nil -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch/test/eunit/couch_work_queue_tests.erl b/src/couch/test/eunit/couch_work_queue_tests.erl index c41cf598e4..85c3f1a3c8 100644 --- a/src/couch/test/eunit/couch_work_queue_tests.erl +++ b/src/couch/test/eunit/couch_work_queue_tests.erl @@ -375,7 +375,7 @@ produce_term(Q, Producer, Term, Wait) -> {item, Ref, Item} -> Item after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -395,7 +395,7 @@ produce(Q, Producer, Size, Wait) -> {item, Ref, Item} -> Item after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch/test/eunit/couchdb_attachments_tests.erl b/src/couch/test/eunit/couchdb_attachments_tests.erl index 5de1eae9e0..103a023268 100644 --- a/src/couch/test/eunit/couchdb_attachments_tests.erl +++ b/src/couch/test/eunit/couchdb_attachments_tests.erl @@ -646,7 +646,7 @@ wait_compaction(DbName, Kind, Line) -> end, case test_util:wait(WaitFun, ?TIMEOUT) of timeout -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, Line}, diff --git a/src/couch/test/eunit/couchdb_file_compression_tests.erl b/src/couch/test/eunit/couchdb_file_compression_tests.erl index 122900d4ba..1016cd2de2 100644 --- a/src/couch/test/eunit/couchdb_file_compression_tests.erl +++ b/src/couch/test/eunit/couchdb_file_compression_tests.erl @@ -222,7 +222,7 @@ wait_compaction(DbName, Kind, Line) -> end, case test_util:wait(WaitFun, ?TIMEOUT * 1000) of timeout -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, Line}, diff --git a/src/couch/test/eunit/couchdb_os_proc_pool.erl b/src/couch/test/eunit/couchdb_os_proc_pool.erl index 1a8479497d..e230e761a1 100644 --- a/src/couch/test/eunit/couchdb_os_proc_pool.erl +++ b/src/couch/test/eunit/couchdb_os_proc_pool.erl @@ -562,7 +562,7 @@ get_client_proc({Pid, Ref}, ClientName) -> receive {proc, Ref, Proc} -> Proc after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -574,7 +574,7 @@ get_client_proc({Pid, Ref}, ClientName) -> end. stop_client({Pid, Ref}) -> - MRef = erlang:monitor(process, Pid), + MRef = monitor(process, Pid), Pid ! stop, receive {stop, Ref} -> @@ -583,12 +583,12 @@ stop_client({Pid, Ref}) -> end, ok after ?TIMEOUT -> - erlang:demonitor(MRef, [flush]), + demonitor(MRef, [flush]), timeout end. kill_client({Pid, Ref}) -> - MRef = erlang:monitor(process, Pid), + MRef = monitor(process, Pid), Pid ! die, receive {die, Ref} -> @@ -597,7 +597,7 @@ kill_client({Pid, Ref}) -> end, ok after ?TIMEOUT -> - erlang:demonitor(MRef, [flush]), + demonitor(MRef, [flush]), timeout end. diff --git a/src/couch/test/eunit/couchdb_update_conflicts_tests.erl b/src/couch/test/eunit/couchdb_update_conflicts_tests.erl index 0722103a4e..fc7884ed90 100644 --- a/src/couch/test/eunit/couchdb_update_conflicts_tests.erl +++ b/src/couch/test/eunit/couchdb_update_conflicts_tests.erl @@ -120,7 +120,7 @@ concurrent_doc_update(NumClients, DbName, InitRev) -> ]} ), Pid = spawn_client(DbName, ClientDoc), - {Value, Pid, erlang:monitor(process, Pid)} + {Value, Pid, monitor(process, Pid)} end, lists:seq(1, NumClients) ), @@ -135,7 +135,7 @@ concurrent_doc_update(NumClients, DbName, InitRev) -> {'DOWN', MonRef, process, Pid, conflict} -> {AccConflicts + 1, AccValue}; {'DOWN', MonRef, process, Pid, Error} -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -146,7 +146,7 @@ concurrent_doc_update(NumClients, DbName, InitRev) -> ]} ) after ?TIMEOUT div 2 -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch/test/eunit/couchdb_vhosts_tests.erl b/src/couch/test/eunit/couchdb_vhosts_tests.erl index d1b7589140..e005537cba 100644 --- a/src/couch/test/eunit/couchdb_vhosts_tests.erl +++ b/src/couch/test/eunit/couchdb_vhosts_tests.erl @@ -96,7 +96,7 @@ should_return_database_info({Url, DbName}) -> {JsonBody} = jiffy:decode(Body), ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -120,7 +120,7 @@ should_return_revs_info({Url, DbName}) -> {JsonBody} = jiffy:decode(Body), ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -146,7 +146,7 @@ should_return_virtual_request_path_field_in_request({Url, DbName}) -> proplists:get_value(<<"requested_path">>, Json) ); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -170,7 +170,7 @@ should_return_real_request_path_field_in_request({Url, DbName}) -> Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -194,7 +194,7 @@ should_match_wildcard_vhost({Url, DbName}) -> Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -218,7 +218,7 @@ should_return_db_info_for_wildcard_vhost_for_custom_db({Url, DbName}) -> {JsonBody} = jiffy:decode(Body), ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -243,7 +243,7 @@ should_replace_rewrite_variables_for_db_and_doc({Url, DbName}) -> Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -267,7 +267,7 @@ should_return_db_info_for_vhost_with_resource({Url, DbName}) -> {JsonBody} = jiffy:decode(Body), ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -291,7 +291,7 @@ should_return_revs_info_for_vhost_with_resource({Url, DbName}) -> {JsonBody} = jiffy:decode(Body), ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -311,7 +311,7 @@ should_return_db_info_for_vhost_with_wildcard_resource({Url, DbName}) -> {JsonBody} = jiffy:decode(Body), ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -335,7 +335,7 @@ should_return_path_for_vhost_with_wildcard_host({Url, DbName}) -> Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); Else -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch/test/eunit/couchdb_views_tests.erl b/src/couch/test/eunit/couchdb_views_tests.erl index 0d32d7fcf5..8b1950948c 100644 --- a/src/couch/test/eunit/couchdb_views_tests.erl +++ b/src/couch/test/eunit/couchdb_views_tests.erl @@ -728,7 +728,7 @@ couchdb_1283() -> ), % Start and pause compacton - WaitRef = erlang:make_ref(), + WaitRef = make_ref(), meck:expect(couch_mrview_index, compact, fun(Db, State, Opts) -> receive {WaitRef, From, init} -> ok @@ -741,7 +741,7 @@ couchdb_1283() -> end), {ok, CPid} = gen_server:call(Pid, compact), - CRef = erlang:monitor(process, CPid), + CRef = monitor(process, CPid), ?assert(is_process_alive(CPid)), % Make sure that our compactor is waiting for us @@ -773,7 +773,7 @@ wait_for_process_shutdown(Pid, ExpectedReason, Error) -> {'DOWN', Pid, process, _, Reason} -> ?assertEqual(ExpectedReason, Reason) after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [{module, ?MODULE}, {line, ?LINE}, Error]} ) end. @@ -994,7 +994,7 @@ compact_db(DbName) -> wait_db_compact_done(DbName, ?WAIT_DELAY_COUNT). wait_db_compact_done(_DbName, 0) -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -1020,7 +1020,7 @@ compact_view_group(DbName, DDocId) when is_binary(DDocId) -> wait_view_compact_done(DbName, DDocId, 10). wait_view_compact_done(_DbName, _DDocId, 0) -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -1052,7 +1052,7 @@ read_header(File) -> stop_indexer(StopFun, Pid, Line, Reason) -> case test_util:stop_sync(Pid, StopFun) of timeout -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, Line}, diff --git a/src/couch_event/src/couch_event_listener.erl b/src/couch_event/src/couch_event_listener.erl index 22bf233274..87d17b6338 100644 --- a/src/couch_event/src/couch_event_listener.erl +++ b/src/couch_event/src/couch_event_listener.erl @@ -47,7 +47,7 @@ term(). start(Mod, Arg, Options) -> - Pid = erlang:spawn(?MODULE, do_init, [Mod, Arg, Options]), + Pid = spawn(?MODULE, do_init, [Mod, Arg, Options]), {ok, Pid}. start(Name, Mod, Arg, Options) -> @@ -59,7 +59,7 @@ start(Name, Mod, Arg, Options) -> end. start_link(Mod, Arg, Options) -> - Pid = erlang:spawn_link(?MODULE, do_init, [Mod, Arg, Options]), + Pid = spawn_link(?MODULE, do_init, [Mod, Arg, Options]), {ok, Pid}. start_link(Name, Mod, Arg, Options) -> @@ -87,7 +87,7 @@ do_init(Module, Arg, Options) -> {ok, State, Timeout} when is_integer(Timeout), Timeout >= 0 -> ?MODULE:loop(#st{module = Module, state = State}, Timeout); Else -> - erlang:exit(Else) + exit(Else) end. loop(St, Timeout) -> @@ -109,7 +109,7 @@ maybe_name_process(Options) -> true -> ok; {false, Pid} -> - erlang:error({already_started, Pid}) + error({already_started, Pid}) end; none -> ok @@ -133,7 +133,7 @@ do_event(#st{module = Module, state = State} = St, DbName, Event) -> {stop, Reason, NewState} -> do_terminate(Reason, St#st{state = NewState}); Else -> - erlang:error(Else) + error(Else) end. do_cast(#st{module = Module, state = State} = St, Message) -> @@ -145,7 +145,7 @@ do_cast(#st{module = Module, state = State} = St, Message) -> {stop, Reason, NewState} -> do_terminate(Reason, St#st{state = NewState}); Else -> - erlang:error(Else) + error(Else) end. do_info(#st{module = Module, state = State} = St, Message) -> @@ -157,7 +157,7 @@ do_info(#st{module = Module, state = State} = St, Message) -> {stop, Reason, NewState} -> do_terminate(Reason, St#st{state = NewState}); Else -> - erlang:error(Else) + error(Else) end. do_terminate(Reason, #st{module = Module, state = State}) -> @@ -173,7 +173,7 @@ do_terminate(Reason, #st{module = Module, state = State}) -> ignore -> normal; Else -> Else end, - erlang:exit(Status). + exit(Status). where({local, Name}) -> whereis(Name). @@ -192,7 +192,7 @@ get_all_dbnames(Options) -> end. get_all_dbnames([], []) -> - erlang:error(no_dbnames_provided); + error(no_dbnames_provided); get_all_dbnames([], Acc) -> lists:usort(convert_dbname_list(Acc)); get_all_dbnames([{dbname, DbName} | Rest], Acc) -> @@ -209,4 +209,4 @@ convert_dbname_list([DbName | Rest]) when is_binary(DbName) -> convert_dbname_list([DbName | Rest]) when is_list(DbName) -> [list_to_binary(DbName) | convert_dbname_list(Rest)]; convert_dbname_list([DbName | _]) -> - erlang:error({invalid_dbname, DbName}). + error({invalid_dbname, DbName}). diff --git a/src/couch_event/src/couch_event_listener_mfa.erl b/src/couch_event/src/couch_event_listener_mfa.erl index 5ec465cf75..55099c2c4c 100644 --- a/src/couch_event/src/couch_event_listener_mfa.erl +++ b/src/couch_event/src/couch_event_listener_mfa.erl @@ -47,7 +47,7 @@ enter_loop(Mod, Func, State, Options) -> Parent = case proplists:get_value(parent, Options) of P when is_pid(P) -> - erlang:monitor(process, P), + monitor(process, P), P; _ -> undefined @@ -64,7 +64,7 @@ stop(Pid) -> couch_event_listener:cast(Pid, shutdown). init({Parent, Mod, Func, State}) -> - erlang:monitor(process, Parent), + monitor(process, Parent), {ok, #st{ mod = Mod, func = Func, @@ -86,7 +86,7 @@ handle_event(DbName, Event, #st{mod = Mod, func = Func, state = State} = St) -> couch_log:error("~p: else in handle_event for db ~p, event ~p, else ~p", [ ?MODULE, DbName, Event, Else ]), - erlang:error(Else) + error(Else) end catch Class:Reason:Stack -> diff --git a/src/couch_event/src/couch_event_server.erl b/src/couch_event/src/couch_event_server.erl index dfdf0ac020..f147306883 100644 --- a/src/couch_event/src/couch_event_server.erl +++ b/src/couch_event/src/couch_event_server.erl @@ -42,7 +42,7 @@ init(_) -> handle_call({register, Pid, NewDbNames}, _From, St) -> case maps:get(Pid, St#st.by_pid, undefined) of undefined -> - NewRef = erlang:monitor(process, Pid), + NewRef = monitor(process, Pid), {reply, ok, register(St, NewRef, Pid, NewDbNames)}; {ReuseRef, OldDbNames} -> unregister(St, Pid, OldDbNames), @@ -53,7 +53,7 @@ handle_call({unregister, Pid}, _From, #st{by_pid = ByPid} = St) -> undefined -> {reply, not_registered, St}; {Ref, OldDbNames} -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), {reply, ok, unregister(St, Pid, OldDbNames)} end; handle_call(Msg, From, St) -> @@ -229,7 +229,7 @@ t_invalid_gen_server_messages(_) -> kill_sync(Pid) -> unlink(Pid), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), exit(Pid, kill), receive {'DOWN', Ref, _, _, _} -> ok diff --git a/src/couch_index/src/couch_index.erl b/src/couch_index/src/couch_index.erl index b8bb351832..0d0e62fb5f 100644 --- a/src/couch_index/src/couch_index.erl +++ b/src/couch_index/src/couch_index.erl @@ -61,7 +61,7 @@ compact(Pid) -> compact(Pid, Options) -> {ok, CPid} = gen_server:call(Pid, compact), case lists:member(monitor, Options) of - true -> {ok, erlang:monitor(process, CPid)}; + true -> {ok, monitor(process, CPid)}; false -> ok end. @@ -381,7 +381,7 @@ send_replies(Waiters, UpdateSeq, IdxState) -> assert_signature_match(Mod, OldIdxState, NewIdxState) -> case {Mod:get(signature, OldIdxState), Mod:get(signature, NewIdxState)} of {Sig, Sig} -> ok; - _ -> erlang:error(signature_mismatch) + _ -> error(signature_mismatch) end. commit_compacted(NewIdxState, State) -> diff --git a/src/couch_index/src/couch_index_server.erl b/src/couch_index/src/couch_index_server.erl index 973b063374..d4593ee0d5 100644 --- a/src/couch_index/src/couch_index_server.erl +++ b/src/couch_index/src/couch_index_server.erl @@ -78,7 +78,7 @@ get_index(Module, <<"shards/", _/binary>> = DbName, DDoc) -> {'DOWN', Ref, process, Pid, Error} -> Error after 61000 -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), {error, timeout} end; get_index(Module, DbName, DDoc) when is_binary(DbName) -> diff --git a/src/couch_index/src/couch_index_util.erl b/src/couch_index/src/couch_index_util.erl index db8aad470e..9a16d06d67 100644 --- a/src/couch_index/src/couch_index_util.erl +++ b/src/couch_index/src/couch_index_util.erl @@ -14,6 +14,7 @@ -export([root_dir/0, index_dir/2, index_file/3]). -export([load_doc/3, sort_lib/1, hexsig/1]). +-export([get_purge_checkpoints/2, cleanup_purges/3]). -include_lib("couch/include/couch_db.hrl"). @@ -72,3 +73,49 @@ sort_lib([{LName, LCode} | Rest], LAcc) -> hexsig(Sig) -> couch_util:to_hex(Sig). + +% Helper function for indexes to get their purge checkpoints as signatures. +% +get_purge_checkpoints(DbName, Type) when is_binary(DbName), is_binary(Type) -> + couch_util:with_db(DbName, fun(Db) -> get_purge_checkpoints(Db, Type) end); +get_purge_checkpoints(Db, Type) when is_binary(Type) -> + Prefix = <>, + PrefixSize = byte_size(Prefix), + FoldFun = fun(#doc{id = Id}, Acc) -> + case Id of + <> -> {ok, Acc#{Sig => Id}}; + _ -> {stop, Acc} + end + end, + Opts = [{start_key, Prefix}], + {ok, Signatures = #{}} = couch_db:fold_local_docs(Db, FoldFun, #{}, Opts), + Signatures. + +% Helper functions for indexes to clean their purge checkpoints. +% +cleanup_purges(DbName, #{} = Sigs, #{} = Checkpoints) when is_binary(DbName) -> + couch_util:with_db(DbName, fun(Db) -> + cleanup_purges(Db, Sigs, Checkpoints) + end); +cleanup_purges(Db, #{} = Sigs, #{} = CheckpointsMap) -> + InactiveMap = maps:without(maps:keys(Sigs), CheckpointsMap), + InactiveCheckpoints = maps:values(InactiveMap), + DeleteFun = fun(DocId) -> delete_checkpoint(Db, DocId) end, + lists:foreach(DeleteFun, InactiveCheckpoints). + +delete_checkpoint(Db, DocId) -> + DbName = couch_db:name(Db), + LogMsg = "~p : deleting inactive purge checkpoint ~s : ~s", + couch_log:debug(LogMsg, [?MODULE, DbName, DocId]), + try couch_db:open_doc(Db, DocId, []) of + {ok, Doc = #doc{}} -> + Deleted = Doc#doc{deleted = true, body = {[]}}, + couch_db:update_doc(Db, Deleted, [?ADMIN_CTX]); + {not_found, _} -> + ok + catch + Tag:Error -> + ErrLog = "~p : error deleting checkpoint ~s : ~s error: ~p:~p", + couch_log:error(ErrLog, [?MODULE, DbName, DocId, Tag, Error]), + ok + end. diff --git a/src/couch_index/test/eunit/couch_index_ddoc_updated_tests.erl b/src/couch_index/test/eunit/couch_index_ddoc_updated_tests.erl index cbdb719545..6b7fe5a4a3 100644 --- a/src/couch_index/test/eunit/couch_index_ddoc_updated_tests.erl +++ b/src/couch_index/test/eunit/couch_index_ddoc_updated_tests.erl @@ -88,7 +88,7 @@ check_all_indexers_exit_on_ddoc_change({_Ctx, DbName}) -> IndexesBefore = get_indexes_by_ddoc(DDocID, N), ?assertEqual(N, length(IndexesBefore)), - AliveBefore = lists:filter(fun erlang:is_process_alive/1, IndexesBefore), + AliveBefore = lists:filter(fun is_process_alive/1, IndexesBefore), ?assertEqual(N, length(AliveBefore)), % update ddoc @@ -121,7 +121,7 @@ check_all_indexers_exit_on_ddoc_change({_Ctx, DbName}) -> ?assertEqual(0, length(IndexesAfter)), %% assert that previously running indexes are gone - AliveAfter = lists:filter(fun erlang:is_process_alive/1, IndexesBefore), + AliveAfter = lists:filter(fun is_process_alive/1, IndexesBefore), ?assertEqual(0, length(AliveAfter)), ok end). diff --git a/src/couch_log/src/couch_log_config.erl b/src/couch_log/src/couch_log_config.erl index c3fd8fe85c..265e4d15fd 100644 --- a/src/couch_log/src/couch_log_config.erl +++ b/src/couch_log/src/couch_log_config.erl @@ -108,7 +108,7 @@ transform(filter_fields, FieldsStr) -> Default = [pid, registered_name, error_info, messages], case parse_term(FieldsStr) of {ok, List} when is_list(List) -> - case lists:all(fun erlang:is_atom/1, List) of + case lists:all(fun is_atom/1, List) of true -> List; false -> diff --git a/src/couch_log/src/couch_log_trunc_io.erl b/src/couch_log/src/couch_log_trunc_io.erl index c330edb336..72ce5390e0 100644 --- a/src/couch_log/src/couch_log_trunc_io.erl +++ b/src/couch_log/src/couch_log_trunc_io.erl @@ -69,7 +69,7 @@ format(Fmt, Args, Max, Options) -> couch_log_trunc_io_fmt:format(Fmt, Args, Max, Options) catch _What:_Why -> - erlang:error(badarg, [Fmt, Args]) + error(badarg, [Fmt, Args]) end. %% @doc Returns an flattened list containing the ASCII representation of the given @@ -114,7 +114,7 @@ print(Term, Max, Options) when is_list(Options) -> print(Term, _Max, #print_options{force_strings = true}) when not is_list(Term), not is_binary(Term), not is_atom(Term) -> - erlang:error(badarg); + error(badarg); print(_, Max, _Options) when Max < 0 -> {"...", 3}; print(_, _, #print_options{depth = 0}) -> {"...", 3}; @@ -249,7 +249,7 @@ print(Fun, Max, _Options) when is_function(Fun) -> L = erlang:fun_to_list(Fun), case length(L) > Max of true -> - S = erlang:max(5, Max), + S = max(5, Max), Res = string:substr(L, 1, S) ++ "..>", {Res, length(Res)}; _ -> @@ -347,7 +347,7 @@ list_bodyc(X, Max, Options, _Tuple) -> {[$|, List], Len + 1}. map_body(Map, Max, #print_options{depth = Depth}) when Max < 4; Depth =:= 0 -> - case erlang:map_size(Map) of + case map_size(Map) of 0 -> {[], 0}; _ -> {"...", 3} end; @@ -474,7 +474,7 @@ alist([H | T], Max, Options = #print_options{force_strings = true}) when is_bina {[List | Final], FLen + Len} end; alist(_, _, #print_options{force_strings = true}) -> - erlang:error(badarg); + error(badarg); alist([H | _L], _Max, _Options) -> throw({unprintable, H}); alist(H, _Max, _Options) -> diff --git a/src/couch_log/src/couch_log_trunc_io_fmt.erl b/src/couch_log/src/couch_log_trunc_io_fmt.erl index 40f3248c23..3c772bd6d4 100644 --- a/src/couch_log/src/couch_log_trunc_io_fmt.erl +++ b/src/couch_log/src/couch_log_trunc_io_fmt.erl @@ -56,10 +56,10 @@ format(FmtStr, Args, MaxLen, Opts) when is_list(FmtStr) -> ), build2(Cs2, Count, MaxLen2 - StrLen); false -> - erlang:error(badarg) + error(badarg) end; format(_FmtStr, _Args, _MaxLen, _Opts) -> - erlang:error(badarg). + error(badarg). collect([$~ | Fmt0], Args0) -> {C, Fmt1, Args1} = collect_cseq(Fmt0, Args0), @@ -324,7 +324,7 @@ term(T, F, Adj, P0, Pad) -> L = lists:flatlength(T), P = case P0 of - none -> erlang:min(L, F); + none -> min(L, F); _ -> P0 end, if @@ -501,10 +501,10 @@ unprefixed_integer(Int, F, Adj, Base, Pad, Lowercase) when -> if Int < 0 -> - S = cond_lowercase(erlang:integer_to_list(-Int, Base), Lowercase), + S = cond_lowercase(integer_to_list(-Int, Base), Lowercase), term([$- | S], F, Adj, none, Pad); true -> - S = cond_lowercase(erlang:integer_to_list(Int, Base), Lowercase), + S = cond_lowercase(integer_to_list(Int, Base), Lowercase), term(S, F, Adj, none, Pad) end. @@ -516,10 +516,10 @@ prefixed_integer(Int, F, Adj, Base, Pad, Prefix, Lowercase) when -> if Int < 0 -> - S = cond_lowercase(erlang:integer_to_list(-Int, Base), Lowercase), + S = cond_lowercase(integer_to_list(-Int, Base), Lowercase), term([$-, Prefix | S], F, Adj, none, Pad); true -> - S = cond_lowercase(erlang:integer_to_list(Int, Base), Lowercase), + S = cond_lowercase(integer_to_list(Int, Base), Lowercase), term([Prefix | S], F, Adj, none, Pad) end. diff --git a/src/couch_log/test/eunit/couch_log_config_listener_test.erl b/src/couch_log/test/eunit/couch_log_config_listener_test.erl index c955972ff7..2e4a45be32 100644 --- a/src/couch_log/test/eunit/couch_log_config_listener_test.erl +++ b/src/couch_log/test/eunit/couch_log_config_listener_test.erl @@ -29,14 +29,14 @@ check_restart_listener() -> Handler1 = get_handler(), ?assertNotEqual(not_found, Handler1), - Ref = erlang:monitor(process, Listener1), + Ref = monitor(process, Listener1), ok = gen_event:delete_handler(config_event, get_handler(), testing), receive {'DOWN', Ref, process, _, _} -> ?assertNot(is_process_alive(Listener1)) after ?TIMEOUT -> - erlang:error({timeout, config_listener_mon_death}) + error({timeout, config_listener_mon_death}) end, NewHandler = test_util:wait( diff --git a/src/couch_log/test/eunit/couch_log_formatter_test.erl b/src/couch_log/test/eunit/couch_log_formatter_test.erl index cdb7eae312..d09f3b38aa 100644 --- a/src/couch_log/test/eunit/couch_log_formatter_test.erl +++ b/src/couch_log/test/eunit/couch_log_formatter_test.erl @@ -53,7 +53,7 @@ crashing_formatting_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** Generic server and some stuff", @@ -76,7 +76,7 @@ gen_server_error_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** Generic server and some stuff", @@ -102,7 +102,7 @@ gen_server_error_with_extra_args_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** Generic server and some stuff", @@ -128,7 +128,7 @@ gen_fsm_error_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** State machine did a thing", @@ -154,7 +154,7 @@ gen_fsm_error_with_extra_args_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** State machine did a thing", @@ -180,7 +180,7 @@ gen_event_error_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** gen_event handler did a thing", @@ -210,7 +210,7 @@ gen_event_error_test() -> emulator_error_test() -> Event = { error, - erlang:group_leader(), + group_leader(), { emulator, "~s~n", @@ -230,7 +230,7 @@ normal_error_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "format thing: ~w ~w", @@ -253,7 +253,7 @@ error_report_std_error_test() -> Pid = self(), Event = { error_report, - erlang:group_leader(), + group_leader(), { Pid, std_error, @@ -274,7 +274,7 @@ supervisor_report_test() -> % A standard supervisor report Event1 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, supervisor_report, @@ -307,7 +307,7 @@ supervisor_report_test() -> % in the offender blob. Event2 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, supervisor_report, @@ -339,7 +339,7 @@ supervisor_report_test() -> % A supervisor_bridge Event3 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, supervisor_report, @@ -370,7 +370,7 @@ supervisor_report_test() -> % Any other supervisor report Event4 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, supervisor_report, @@ -391,7 +391,7 @@ crash_report_test() -> % A standard crash report Event1 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, crash_report, @@ -425,7 +425,7 @@ crash_report_test() -> % A registered process crash report Event2 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, crash_report, @@ -450,7 +450,7 @@ crash_report_test() -> % A non-exit crash report Event3 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, crash_report, @@ -475,7 +475,7 @@ crash_report_test() -> % A extra report info Event4 = { error_report, - erlang:group_leader(), + group_leader(), { Pid, crash_report, @@ -504,7 +504,7 @@ warning_report_test() -> % A warning message Event1 = { warning_msg, - erlang:group_leader(), + group_leader(), { Pid, "a ~s string ~w", @@ -522,7 +522,7 @@ warning_report_test() -> % A warning report Event2 = { warning_report, - erlang:group_leader(), + group_leader(), { Pid, std_warning, @@ -543,7 +543,7 @@ info_report_test() -> % An info message Event1 = { info_msg, - erlang:group_leader(), + group_leader(), { Pid, "an info ~s string ~w", @@ -561,7 +561,7 @@ info_report_test() -> % Application exit info Event2 = { info_report, - erlang:group_leader(), + group_leader(), { Pid, std_info, @@ -583,7 +583,7 @@ info_report_test() -> % Any other std_info message Event3 = { info_report, - erlang:group_leader(), + group_leader(), { Pid, std_info, @@ -604,7 +604,7 @@ info_report_test() -> % Non-list other report Event4 = { info_report, - erlang:group_leader(), + group_leader(), { Pid, std_info, @@ -625,7 +625,7 @@ progress_report_test() -> % Application started Event1 = { info_report, - erlang:group_leader(), + group_leader(), { Pid, progress, @@ -643,7 +643,7 @@ progress_report_test() -> % Supervisor started child Event2 = { info_report, - erlang:group_leader(), + group_leader(), { Pid, progress, @@ -669,7 +669,7 @@ progress_report_test() -> % Other progress report Event3 = { info_report, - erlang:group_leader(), + group_leader(), { Pid, progress, @@ -787,7 +787,7 @@ format_reason_test_() -> "bad argument in call to j:k(a, 2) at y:x/1" }, { - {{badarity, {fun erlang:spawn/1, [a, b]}}, [{y, x, 1}]}, + {{badarity, {fun spawn/1, [a, b]}}, [{y, x, 1}]}, "function called with wrong arity of 2 instead of 1 at y:x/1" }, { @@ -837,7 +837,7 @@ coverage_test() -> do_format( { error_report, - erlang:group_leader(), + group_leader(), {self(), std_error, "foobar"} } ) @@ -852,7 +852,7 @@ coverage_test() -> do_format( { error_report, - erlang:group_leader(), + group_leader(), {self(), std_error, dang} } ) @@ -862,7 +862,7 @@ gen_server_error_with_last_msg_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** Generic server and some stuff", @@ -890,7 +890,7 @@ gen_event_error_with_last_msg_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** gen_event handler did a thing", @@ -923,7 +923,7 @@ gen_fsm_error_with_last_msg_test() -> Pid = self(), Event = { error, - erlang:group_leader(), + group_leader(), { Pid, "** State machine did a thing", diff --git a/src/couch_log/test/eunit/couch_log_server_test.erl b/src/couch_log/test/eunit/couch_log_server_test.erl index df768f26ea..ff9dbc3f0d 100644 --- a/src/couch_log/test/eunit/couch_log_server_test.erl +++ b/src/couch_log/test/eunit/couch_log_server_test.erl @@ -39,7 +39,7 @@ check_can_reconfigure() -> check_can_restart() -> Pid1 = whereis(couch_log_server), - Ref = erlang:monitor(process, Pid1), + Ref = monitor(process, Pid1), ?assert(is_process_alive(Pid1)), supervisor:terminate_child(couch_log_sup, couch_log_server), @@ -48,7 +48,7 @@ check_can_restart() -> receive {'DOWN', Ref, _, _, _} -> ok after 1000 -> - erlang:error(timeout_restarting_couch_log_server) + error(timeout_restarting_couch_log_server) end, ?assert(not is_process_alive(Pid1)), diff --git a/src/couch_log/test/eunit/couch_log_test_util.erl b/src/couch_log/test/eunit/couch_log_test_util.erl index 9a170bdbdb..743cdf6eff 100644 --- a/src/couch_log/test/eunit/couch_log_test_util.erl +++ b/src/couch_log/test/eunit/couch_log_test_util.erl @@ -64,7 +64,7 @@ wait_for_config() -> receive couch_log_config_change_finished -> ok after 1000 -> - erlang:error(config_change_timeout) + error(config_change_timeout) end. with_meck(Mods, Fun) -> @@ -119,7 +119,7 @@ disable_logs_from(Name) when is_atom(Name) -> P when is_pid(P) -> disable_logs_from(P); undefined -> - erlang:error({unknown_pid_name, Name}) + error({unknown_pid_name, Name}) end. last_log_key() -> diff --git a/src/couch_mrview/src/couch_mrview.erl b/src/couch_mrview/src/couch_mrview.erl index 037c35cdd1..bc7b1f8abf 100644 --- a/src/couch_mrview/src/couch_mrview.erl +++ b/src/couch_mrview/src/couch_mrview.erl @@ -183,7 +183,7 @@ map_function_type({Props}) -> end. format_type(Type) when is_atom(Type) -> - ?l2b(atom_to_list(Type)); + atom_to_binary(Type); format_type(Types) when is_list(Types) -> iolist_to_binary(join(lists:map(fun atom_to_list/1, Types), <<" or ">>)). @@ -287,12 +287,22 @@ query_all_docs(Db, Args0, Callback, Acc) -> couch_index_util:hexsig(couch_hash:md5_hash(?term_to_bin(Info))) end), Args1 = Args0#mrargs{view_type = map}, + + % TODO: Compatibility clause. Remove after upgrading to next minor release + % after 3.5.0. + % + % As of commit 7aa8a4, args are validated in fabric. However, during online + % cluster upgrades, old nodes will still expect args to be validated on + % workers, so keep the clause around until the next minor version then + % remove. + % + Args2 = couch_mrview_util:validate_all_docs_args(Db, Args1), {ok, Acc1} = - case Args1#mrargs.preflight_fun of + case Args2#mrargs.preflight_fun of PFFun when is_function(PFFun, 2) -> PFFun(Sig, Acc); _ -> {ok, Acc} end, - all_docs_fold(Db, Args1, Callback, Acc1). + all_docs_fold(Db, Args2, Callback, Acc1). query_view(Db, DDoc, VName) -> Args = #mrargs{extra = [{view_row_map, true}]}, @@ -328,7 +338,7 @@ query_view(Db, {Type, View, Ref}, Args, Callback, Acc) -> red -> red_fold(Db, View, Args, Callback, Acc) end after - erlang:demonitor(Ref, [flush]) + demonitor(Ref, [flush]) end. get_info(Db, DDoc) -> diff --git a/src/couch_mrview/src/couch_mrview_cleanup.erl b/src/couch_mrview/src/couch_mrview_cleanup.erl index 5b5afbdce0..e8a2833a7c 100644 --- a/src/couch_mrview/src/couch_mrview_cleanup.erl +++ b/src/couch_mrview/src/couch_mrview_cleanup.erl @@ -14,12 +14,9 @@ -export([ run/1, - cleanup_purges/3, - cleanup_indices/2 + cleanup/2 ]). --include_lib("couch/include/couch_db.hrl"). - run(Db) -> Indices = couch_mrview_util:get_index_files(Db), Checkpoints = couch_mrview_util:get_purge_checkpoints(Db), @@ -28,15 +25,26 @@ run(Db) -> ok = cleanup_purges(Db1, Sigs, Checkpoints), ok = cleanup_indices(Sigs, Indices). -cleanup_purges(DbName, Sigs, Checkpoints) when is_binary(DbName) -> - couch_util:with_db(DbName, fun(Db) -> - cleanup_purges(Db, Sigs, Checkpoints) - end); -cleanup_purges(Db, #{} = Sigs, #{} = CheckpointsMap) -> - InactiveMap = maps:without(maps:keys(Sigs), CheckpointsMap), - InactiveCheckpoints = maps:values(InactiveMap), - DeleteFun = fun(DocId) -> delete_checkpoint(Db, DocId) end, - lists:foreach(DeleteFun, InactiveCheckpoints). +% erpc endpoint for fabric_index_cleanup:cleanup_indexes/2 +% +cleanup(Dbs, #{} = Sigs) -> + try + lists:foreach( + fun(Db) -> + Indices = couch_mrview_util:get_index_files(Db), + Checkpoints = couch_mrview_util:get_purge_checkpoints(Db), + ok = cleanup_purges(Db, Sigs, Checkpoints), + ok = cleanup_indices(Sigs, Indices) + end, + Dbs + ) + catch + error:database_does_not_exist -> + ok + end. + +cleanup_purges(Db, Sigs, Checkpoints) -> + couch_index_util:cleanup_purges(Db, Sigs, Checkpoints). cleanup_indices(#{} = Sigs, #{} = IndexMap) -> Fun = fun(_, Files) -> lists:foreach(fun delete_file/1, Files) end, @@ -54,20 +62,3 @@ delete_file(File) -> couch_log:error(ErrLog, [?MODULE, File, Tag, Error]), ok end. - -delete_checkpoint(Db, DocId) -> - DbName = couch_db:name(Db), - LogMsg = "~p : deleting inactive purge checkpoint ~s : ~s", - couch_log:debug(LogMsg, [?MODULE, DbName, DocId]), - try couch_db:open_doc(Db, DocId, []) of - {ok, Doc = #doc{}} -> - Deleted = Doc#doc{deleted = true, body = {[]}}, - couch_db:update_doc(Db, Deleted, [?ADMIN_CTX]); - {not_found, _} -> - ok - catch - Tag:Error -> - ErrLog = "~p : error deleting checkpoint ~s : ~s error: ~p:~p", - couch_log:error(ErrLog, [?MODULE, DbName, DocId, Tag, Error]), - ok - end. diff --git a/src/couch_mrview/src/couch_mrview_compactor.erl b/src/couch_mrview/src/couch_mrview_compactor.erl index 28e5a9b3da..a534dd0e8a 100644 --- a/src/couch_mrview/src/couch_mrview_compactor.erl +++ b/src/couch_mrview/src/couch_mrview_compactor.erl @@ -136,11 +136,11 @@ recompact(State) -> recompact(State, recompact_retry_count()). recompact(#mrst{db_name = DbName, idx_name = IdxName}, 0) -> - erlang:error({exceeded_recompact_retry_count, [{db_name, DbName}, {idx_name, IdxName}]}); + error({exceeded_recompact_retry_count, [{db_name, DbName}, {idx_name, IdxName}]}); recompact(State, RetryCount) -> Self = self(), link(State#mrst.fd), - {Pid, Ref} = erlang:spawn_monitor(fun() -> + {Pid, Ref} = spawn_monitor(fun() -> couch_index_updater:update(Self, couch_mrview_index, State) end), recompact_loop(Pid, Ref, State, RetryCount). @@ -240,7 +240,7 @@ swap_compacted(OldState, NewState) -> } = NewState, link(NewState#mrst.fd), - Ref = erlang:monitor(process, NewState#mrst.fd), + Ref = monitor(process, NewState#mrst.fd), RootDir = couch_index_util:root_dir(), IndexFName = couch_mrview_util:index_file(DbName, Sig), @@ -257,7 +257,7 @@ swap_compacted(OldState, NewState) -> ok = file:rename(CompactFName, IndexFName), unlink(OldState#mrst.fd), - erlang:demonitor(OldState#mrst.fd_monitor, [flush]), + demonitor(OldState#mrst.fd_monitor, [flush]), {ok, NewState#mrst{fd_monitor = Ref}}. diff --git a/src/couch_mrview/src/couch_mrview_index.erl b/src/couch_mrview/src/couch_mrview_index.erl index 51777480cd..a5080ed763 100644 --- a/src/couch_mrview/src/couch_mrview_index.erl +++ b/src/couch_mrview/src/couch_mrview_index.erl @@ -167,7 +167,7 @@ open(Db, State0) -> end. close(State) -> - erlang:demonitor(State#mrst.fd_monitor, [flush]), + demonitor(State#mrst.fd_monitor, [flush]), couch_file:close(State#mrst.fd). % This called after ddoc_updated event occurrs, and @@ -178,7 +178,7 @@ close(State) -> % couch_file will be closed automatically after all % outstanding queries are done. shutdown(State) -> - erlang:demonitor(State#mrst.fd_monitor, [flush]), + demonitor(State#mrst.fd_monitor, [flush]), unlink(State#mrst.fd). delete(#mrst{db_name = DbName, sig = Sig} = State) -> diff --git a/src/couch_mrview/src/couch_mrview_test_util.erl b/src/couch_mrview/src/couch_mrview_test_util.erl index 349611191b..f021ce58d9 100644 --- a/src/couch_mrview/src/couch_mrview_test_util.erl +++ b/src/couch_mrview/src/couch_mrview_test_util.erl @@ -139,7 +139,7 @@ ddoc(Id) -> doc(Id) -> couch_doc:from_json_obj( {[ - {<<"_id">>, list_to_binary(integer_to_list(Id))}, + {<<"_id">>, integer_to_binary(Id)}, {<<"val">>, Id} ]} ). diff --git a/src/couch_mrview/src/couch_mrview_updater.erl b/src/couch_mrview/src/couch_mrview_updater.erl index 92e66d0fa3..4238e6b7db 100644 --- a/src/couch_mrview/src/couch_mrview_updater.erl +++ b/src/couch_mrview/src/couch_mrview_updater.erl @@ -166,13 +166,13 @@ map_docs(Parent, #mrst{db_name = DbName, idx_name = IdxName} = State0) -> QServer = State1#mrst.qserver, DocFun = fun ({nil, Seq, _}, {SeqAcc, Results}) -> - {erlang:max(Seq, SeqAcc), Results}; + {max(Seq, SeqAcc), Results}; ({Id, Seq, deleted}, {SeqAcc, Results}) -> - {erlang:max(Seq, SeqAcc), [{Id, []} | Results]}; + {max(Seq, SeqAcc), [{Id, []} | Results]}; ({Id, Seq, Doc}, {SeqAcc, Results}) -> couch_stats:increment_counter([couchdb, mrview, map_doc]), {ok, Res} = couch_query_servers:map_doc_raw(QServer, Doc), - {erlang:max(Seq, SeqAcc), [{Id, Res} | Results]} + {max(Seq, SeqAcc), [{Id, Res} | Results]} end, FoldFun = fun(Docs, Acc) -> update_task(length(Docs)), @@ -242,7 +242,7 @@ merge_results([{Seq, Results} | Rest], SeqAcc, ViewKVs, DocIdKeys) -> merge_results(RawResults, VKV, DIK) end, {ViewKVs1, DocIdKeys1} = lists:foldl(Fun, {ViewKVs, DocIdKeys}, Results), - merge_results(Rest, erlang:max(Seq, SeqAcc), ViewKVs1, DocIdKeys1). + merge_results(Rest, max(Seq, SeqAcc), ViewKVs1, DocIdKeys1). merge_results({DocId, []}, ViewKVs, DocIdKeys) -> {ViewKVs, [{DocId, []} | DocIdKeys]}; diff --git a/src/couch_mrview/src/couch_mrview_util.erl b/src/couch_mrview/src/couch_mrview_util.erl index a5c81a0736..5405e8db82 100644 --- a/src/couch_mrview/src/couch_mrview_util.erl +++ b/src/couch_mrview/src/couch_mrview_util.erl @@ -16,6 +16,7 @@ -export([get_local_purge_doc_id/1, get_value_from_options/2]). -export([verify_view_filename/1, get_signature_from_filename/1]). -export([get_signatures/1, get_purge_checkpoints/1, get_index_files/1]). +-export([get_signatures_from_ddocs/2]). -export([ddoc_to_mrst/2, init_state/4, reset_index/3]). -export([make_header/1]). -export([index_file/2, compaction_file/2, open_file/1]). @@ -94,40 +95,35 @@ get_signatures(DbName) when is_binary(DbName) -> couch_util:with_db(DbName, fun get_signatures/1); get_signatures(Db) -> DbName = couch_db:name(Db), - % get_design_docs/1 returns ejson for clustered shards, and - % #full_doc_info{}'s for other cases. {ok, DDocs} = couch_db:get_design_docs(Db), + % get_design_docs/1 returns ejson for clustered shards, and + % #full_doc_info{}'s for other cases. Both are transformed to #doc{} records FoldFun = fun ({[_ | _]} = EJsonDoc, Acc) -> Doc = couch_doc:from_json_obj(EJsonDoc), - {ok, Mrst} = ddoc_to_mrst(DbName, Doc), - Sig = couch_util:to_hex_bin(Mrst#mrst.sig), - Acc#{Sig => true}; + [Doc | Acc]; (#full_doc_info{} = FDI, Acc) -> {ok, Doc} = couch_db:open_doc_int(Db, FDI, [ejson_body]), - {ok, Mrst} = ddoc_to_mrst(DbName, Doc), - Sig = couch_util:to_hex_bin(Mrst#mrst.sig), - Acc#{Sig => true} + [Doc | Acc] + end, + DDocs1 = lists:foldl(FoldFun, [], DDocs), + get_signatures_from_ddocs(DbName, DDocs1). + +% From a list of design #doc{} records returns signatures map: #{Sig => true} +% +get_signatures_from_ddocs(DbName, DDocs) when is_list(DDocs) -> + FoldFun = fun(#doc{} = Doc, Acc) -> + {ok, Mrst} = ddoc_to_mrst(DbName, Doc), + Sig = couch_util:to_hex_bin(Mrst#mrst.sig), + Acc#{Sig => true} end, lists:foldl(FoldFun, #{}, DDocs). % Returns a map of `Sig => DocId` elements for all the purge view % checkpoint docs. Sig is a hex-encoded binary. % -get_purge_checkpoints(DbName) when is_binary(DbName) -> - couch_util:with_db(DbName, fun get_purge_checkpoints/1); get_purge_checkpoints(Db) -> - FoldFun = fun(#doc{id = Id}, Acc) -> - case Id of - <> -> - {ok, Acc#{Sig => Id}}; - _ -> - {stop, Acc} - end - end, - Opts = [{start_key, <>}], - {ok, Signatures = #{}} = couch_db:fold_local_docs(Db, FoldFun, #{}, Opts), - Signatures. + couch_index_util:get_purge_checkpoints(Db, <<"mrview">>). % Returns a map of `Sig => [FilePath, ...]` elements. Sig is a hex-encoded % binary and FilePaths are lists as they intended to be passed to couch_file @@ -152,7 +148,7 @@ get_index_files(Db) -> get_view(Db, DDoc, ViewName, Args0) -> case get_view_index_state(Db, DDoc, ViewName, Args0) of {ok, State, Args2} -> - Ref = erlang:monitor(process, State#mrst.fd), + Ref = monitor(process, State#mrst.fd), #mrst{language = Lang, views = Views} = State, {Type, View, Args3} = extract_view(Lang, Args2, ViewName, Views), check_range(Args3, view_cmp(View)), @@ -382,7 +378,7 @@ init_state(Db, Fd, State, Header) -> {ShouldCommit, State#mrst{ fd = Fd, - fd_monitor = erlang:monitor(process, Fd), + fd_monitor = monitor(process, Fd), update_seq = Seq, purge_seq = PurgeSeq, id_btree = IdBtree, @@ -544,7 +540,13 @@ apply_limit(ViewPartitioned, Args) -> mrverror(io_lib:format(Fmt, [MaxLimit])) end. -validate_all_docs_args(Db, Args0) -> +validate_all_docs_args(Db, #mrargs{} = Args) -> + case get_extra(Args, validated, false) of + true -> Args; + false -> validate_all_docs_args_int(Db, Args) + end. + +validate_all_docs_args_int(Db, Args0) -> Args = validate_args(Args0#mrargs{view_type = map}), DbPartitioned = couch_db:is_partitioned(Db), @@ -560,7 +562,13 @@ validate_all_docs_args(Db, Args0) -> apply_limit(false, Args) end. -validate_args(Args) -> +validate_args(#mrargs{} = Args) -> + case get_extra(Args, validated, false) of + true -> Args; + false -> validate_args_int(Args) + end. + +validate_args_int(#mrargs{} = Args) -> GroupLevel = determine_group_level(Args), Reduce = Args#mrargs.reduce, case Reduce == undefined orelse is_boolean(Reduce) of @@ -696,11 +704,12 @@ validate_args(Args) -> _ -> mrverror(<<"Invalid value for `partition`.">>) end, - Args#mrargs{ + Args1 = Args#mrargs{ start_key_docid = SKDocId, end_key_docid = EKDocId, group_level = GroupLevel - }. + }, + set_extra(Args1, validated, true). determine_group_level(#mrargs{group = undefined, group_level = undefined}) -> 0; @@ -1304,7 +1313,8 @@ make_user_reds_reduce_fun(Lang, ReduceFuns, NthRed) -> get_btree_state(nil) -> nil; -get_btree_state(#btree{} = Btree) -> +get_btree_state(Btree) -> + true = couch_btree:is_btree(Btree), couch_btree:get_state(Btree). extract_view_reduce({red, {N, _Lang, #mrview{reduce_funs = Reds}}, _Ref}) -> diff --git a/src/couch_mrview/test/eunit/couch_mrview_collation_tests.erl b/src/couch_mrview/test/eunit/couch_mrview_collation_tests.erl index c00b97b33c..87d4814260 100644 --- a/src/couch_mrview/test/eunit/couch_mrview_collation_tests.erl +++ b/src/couch_mrview/test/eunit/couch_mrview_collation_tests.erl @@ -137,7 +137,7 @@ find_matching_rows(Index, Value) -> ), lists:map( fun({Id, V}) -> - {row, [{id, list_to_binary(integer_to_list(Id))}, {key, V}, {value, 0}]} + {row, [{id, integer_to_binary(Id)}, {key, V}, {value, 0}]} end, Matches ). @@ -206,7 +206,7 @@ make_docs() -> fun(V, {Docs0, Count}) -> Doc = couch_doc:from_json_obj( {[ - {<<"_id">>, list_to_binary(integer_to_list(Count))}, + {<<"_id">>, integer_to_binary(Count)}, {<<"foo">>, V} ]} ), @@ -220,7 +220,7 @@ make_docs() -> rows() -> {Rows, _} = lists:foldl( fun(V, {Rows0, Count}) -> - Id = list_to_binary(integer_to_list(Count)), + Id = integer_to_binary(Count), Row = {row, [{id, Id}, {key, V}, {value, 0}]}, {[Row | Rows0], Count + 1} end, diff --git a/src/couch_mrview/test/eunit/couch_mrview_compact_tests.erl b/src/couch_mrview/test/eunit/couch_mrview_compact_tests.erl index df035c649a..048f6bc701 100644 --- a/src/couch_mrview/test/eunit/couch_mrview_compact_tests.erl +++ b/src/couch_mrview/test/eunit/couch_mrview_compact_tests.erl @@ -55,7 +55,7 @@ should_swap(Db) -> receive {'DOWN', MonRef, process, _, _} -> ok after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -68,7 +68,7 @@ should_swap(Db) -> {QPid, Count} -> ?assertEqual(1000, Count) after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -86,7 +86,7 @@ should_remove(Db) -> ok = couch_index:compact(IndexPid, []), {ok, CompactorPid} = couch_index:get_compactor_pid(IndexPid), {ok, CompactingPid} = couch_index_compactor:get_compacting_pid(CompactorPid), - MonRef = erlang:monitor(process, CompactingPid), + MonRef = monitor(process, CompactingPid), exit(CompactingPid, crash), receive {'DOWN', MonRef, process, _, crash} -> @@ -100,7 +100,7 @@ should_remove(Db) -> ?assert(is_process_alive(IndexPid)), ?assert(is_process_alive(CompactorPid)) after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch_mrview/test/eunit/couch_mrview_ddoc_updated_tests.erl b/src/couch_mrview/test/eunit/couch_mrview_ddoc_updated_tests.erl index 91b24e336a..756434892a 100644 --- a/src/couch_mrview/test/eunit/couch_mrview_ddoc_updated_tests.erl +++ b/src/couch_mrview/test/eunit/couch_mrview_ddoc_updated_tests.erl @@ -90,7 +90,7 @@ check_indexing_stops_on_ddoc_change(Db) -> IndexesBefore = get_indexes_by_ddoc(couch_db:name(Db), DDocID, 1), ?assertEqual(1, length(IndexesBefore)), - AliveBefore = lists:filter(fun erlang:is_process_alive/1, IndexesBefore), + AliveBefore = lists:filter(fun is_process_alive/1, IndexesBefore), ?assertEqual(1, length(AliveBefore)), {ok, DDoc} = couch_db:open_doc(Db, DDocID, [ejson_body, ?ADMIN_CTX]), @@ -117,7 +117,7 @@ check_indexing_stops_on_ddoc_change(Db) -> {QPid, Msg} -> ?assertEqual(Msg, ddoc_updated) after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -129,7 +129,7 @@ check_indexing_stops_on_ddoc_change(Db) -> %% assert that previously running indexes are gone IndexesAfter = get_indexes_by_ddoc(couch_db:name(Db), DDocID, 0), ?assertEqual(0, length(IndexesAfter)), - AliveAfter = lists:filter(fun erlang:is_process_alive/1, IndexesBefore), + AliveAfter = lists:filter(fun is_process_alive/1, IndexesBefore), ?assertEqual(0, length(AliveAfter)) end). diff --git a/src/couch_mrview/test/eunit/couch_mrview_purge_docs_fabric_tests.erl b/src/couch_mrview/test/eunit/couch_mrview_purge_docs_fabric_tests.erl index 3207a3da3d..f3452b55a2 100644 --- a/src/couch_mrview/test/eunit/couch_mrview_purge_docs_fabric_tests.erl +++ b/src/couch_mrview/test/eunit/couch_mrview_purge_docs_fabric_tests.erl @@ -288,7 +288,7 @@ wait_compaction(DbName, Line) -> end, case test_util:wait(WaitFun, 10000) of timeout -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, Line}, diff --git a/src/couch_mrview/test/eunit/couch_mrview_purge_docs_tests.erl b/src/couch_mrview/test/eunit/couch_mrview_purge_docs_tests.erl index 44500bdf1b..c4ac9ed139 100644 --- a/src/couch_mrview/test/eunit/couch_mrview_purge_docs_tests.erl +++ b/src/couch_mrview/test/eunit/couch_mrview_purge_docs_tests.erl @@ -299,7 +299,7 @@ test_purge_index_reset(Db) -> PurgeInfos = lists:map( fun(I) -> - DocId = list_to_binary(integer_to_list(I)), + DocId = integer_to_binary(I), FDI = couch_db:get_full_doc_info(Db, DocId), Rev = get_rev(FDI), {couch_uuids:random(), DocId, [Rev]} @@ -576,7 +576,7 @@ wait_compaction(DbName, Kind, Line) -> end, case test_util:wait(WaitFun, 10000) of timeout -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, Line}, @@ -600,7 +600,7 @@ fold_fun({_PSeq, _UUID, Id, Revs}, Acc) -> {ok, [{Id, Revs} | Acc]}. docid(I) -> - list_to_binary(integer_to_list(I)). + integer_to_binary(I). uuid(I) -> Str = io_lib:format("UUID~4..0b", [I]), diff --git a/src/couch_mrview/test/eunit/couch_mrview_util_tests.erl b/src/couch_mrview/test/eunit/couch_mrview_util_tests.erl index 2562bb511e..c304dcdad3 100644 --- a/src/couch_mrview/test/eunit/couch_mrview_util_tests.erl +++ b/src/couch_mrview/test/eunit/couch_mrview_util_tests.erl @@ -174,3 +174,31 @@ t_get_index_files_clustered({DbName, _Db}) -> ?assertMatch({ok, _}, file:read_file_info(File)), {ok, Info} = couch_mrview:get_info(ShardName1, ?DDOC_ID), ?assertEqual(proplists:get_value(signature, Info), Sig). + +do_not_validate_args_if_already_validated_test() -> + Args = #mrargs{ + view_type = red, + group = true, + group_level = undefined, + extra = [{foo, bar}] + }, + + % Initially if we haven't validated, it's not flagged as such + ?assertNot(couch_mrview_util:get_extra(Args, validated, false)), + + % Do the validation + Args1 = couch_mrview_util:validate_args(Args), + + % Validation worked + ?assertEqual(exact, Args1#mrargs.group_level), + + % Validation flag is set to true + ?assert(couch_mrview_util:get_extra(Args1, validated, false)), + + Args2 = couch_mrview_util:validate_args(Args1), + % No change, as already validated + ?assertEqual(Args1, Args2), + + Args3 = couch_mrview_util:validate_all_docs_args(some_db, Args2), + % No change for all docs validation as already validated + ?assertEqual(Args1, Args3). diff --git a/src/couch_peruser/src/couch_peruser.erl b/src/couch_peruser/src/couch_peruser.erl index 6f4a24cab2..8a7cbe13a8 100644 --- a/src/couch_peruser/src/couch_peruser.erl +++ b/src/couch_peruser/src/couch_peruser.erl @@ -269,7 +269,7 @@ should_handle_doc(ShardName, DocId) -> ) -> boolean(). should_handle_doc_int(ShardName, DocId) -> DbName = mem3:dbname(ShardName), - Live = [erlang:node() | erlang:nodes()], + Live = [erlang:node() | nodes()], Shards = mem3:shards(DbName, DocId), Nodes = [N || #shard{node = N} <- Shards, lists:member(N, Live)], case mem3:owner(DbName, DocId, Nodes) of diff --git a/src/couch_prometheus/src/couch_prometheus.erl b/src/couch_prometheus/src/couch_prometheus.erl index 4d053d1af7..7f3dc494d1 100644 --- a/src/couch_prometheus/src/couch_prometheus.erl +++ b/src/couch_prometheus/src/couch_prometheus.erl @@ -68,7 +68,8 @@ get_system_stats() -> get_internal_replication_jobs_stat(), get_membership_stat(), get_membership_nodes(), - get_distribution_stats() + get_distribution_stats(), + get_bt_engine_cache_stats() ]). get_uptime_stat() -> @@ -119,9 +120,9 @@ get_vm_stats() -> end, erlang:memory() ), - {NumGCs, WordsReclaimed, _} = erlang:statistics(garbage_collection), - CtxSwitches = element(1, erlang:statistics(context_switches)), - Reds = element(1, erlang:statistics(reductions)), + {NumGCs, WordsReclaimed, _} = statistics(garbage_collection), + CtxSwitches = element(1, statistics(context_switches)), + Reds = element(1, statistics(reductions)), ProcCount = erlang:system_info(process_count), ProcLimit = erlang:system_info(process_limit), [ @@ -157,7 +158,7 @@ get_vm_stats() -> ]. get_io_stats() -> - {{input, In}, {output, Out}} = erlang:statistics(io), + {{input, In}, {output, Out}} = statistics(io), [ to_prom( erlang_io_recv_bytes_total, @@ -374,3 +375,12 @@ get_distribution_stats() -> get_ets_stats() -> NumTabs = length(ets:all()), to_prom(erlang_ets_table, gauge, "number of ETS tables", NumTabs). + +get_bt_engine_cache_stats() -> + Stats = couch_bt_engine_cache:info(), + Size = maps:get(size, Stats, 0), + Mem = maps:get(memory, Stats, 0), + [ + to_prom(couchdb_bt_engine_cache_memory, gauge, "memory used by the btree cache", Mem), + to_prom(couchdb_bt_engine_cache_size, gauge, "number of entries in the btree cache", Size) + ]. diff --git a/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl b/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl index c33b379a84..3838717214 100644 --- a/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl +++ b/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl @@ -114,9 +114,9 @@ t_no_duplicate_metrics(Port) -> % definition, not the values. These lines always start with % a # character. MetricDefs = lists:filter(fun(S) -> string:find(S, "#") =:= S end, Lines), - ?assertNotEqual(erlang:length(MetricDefs), 0), + ?assertNotEqual(length(MetricDefs), 0), Diff = get_duplicates(MetricDefs), - ?assertEqual(erlang:length(Diff), 0). + ?assertEqual(length(Diff), 0). get_duplicates(List) -> List -- sets:to_list(couch_util:set_from_list(List)). @@ -158,7 +158,7 @@ t_starts_with_couchdb(Port) -> {match, _} -> ok; nomatch -> - erlang:error( + error( {assertRegexp_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch_pse_tests/src/cpse_test_ref_counting.erl b/src/couch_pse_tests/src/cpse_test_ref_counting.erl index e563210807..401353f37f 100644 --- a/src/couch_pse_tests/src/cpse_test_ref_counting.erl +++ b/src/couch_pse_tests/src/cpse_test_ref_counting.erl @@ -82,7 +82,7 @@ start_client(Db0) -> {waiting, Pid} -> Pid ! go after 1000 -> - erlang:error(timeout) + error(timeout) end, receive @@ -90,7 +90,7 @@ start_client(Db0) -> couch_db:close(Db1), ok after 1000 -> - erlang:error(timeout) + error(timeout) end end). @@ -99,7 +99,7 @@ wait_client({Pid, _Ref}) -> receive go -> ok after 1000 -> - erlang:error(timeout) + error(timeout) end. close_client({Pid, Ref}) -> @@ -108,5 +108,5 @@ close_client({Pid, Ref}) -> {'DOWN', Ref, _, _, _} -> ok after 1000 -> - erlang:error(timeout) + error(timeout) end. diff --git a/src/couch_pse_tests/src/cpse_util.erl b/src/couch_pse_tests/src/cpse_util.erl index c5aac94c0e..bd17de26bc 100644 --- a/src/couch_pse_tests/src/cpse_util.erl +++ b/src/couch_pse_tests/src/cpse_util.erl @@ -95,13 +95,13 @@ open_db(DbName) -> shutdown_db(Db) -> Pid = couch_db:get_pid(Db), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), exit(Pid, kill), receive {'DOWN', Ref, _, _, _} -> ok after ?SHUTDOWN_TIMEOUT -> - erlang:error(database_shutdown_timeout) + error(database_shutdown_timeout) end, test_util:wait(fun() -> case @@ -189,7 +189,7 @@ assert_db_props(Module, Line, DbName, Props) when is_binary(DbName) -> catch error:{assertEqual, Props} -> {_, Rest} = proplists:split(Props, [module, line]), - erlang:error({assertEqual, [{module, Module}, {line, Line} | Rest]}) + error({assertEqual, [{module, Module}, {line, Line} | Rest]}) after couch_db:close(Db) end; @@ -199,7 +199,7 @@ assert_db_props(Module, Line, Db, Props) -> catch error:{assertEqual, Props} -> {_, Rest} = proplists:split(Props, [module, line]), - erlang:error({assertEqual, [{module, Module}, {line, Line} | Rest]}) + error({assertEqual, [{module, Module}, {line, Line} | Rest]}) end. assert_each_prop(_Db, []) -> @@ -309,7 +309,7 @@ gen_write(Db, {Action, {<<"_local/", _/binary>> = DocId, Body}}) -> end, {local, #doc{ id = DocId, - revs = {0, [list_to_binary(integer_to_list(RevId))]}, + revs = {0, [integer_to_binary(RevId)]}, body = Body, deleted = Deleted }}; @@ -387,7 +387,7 @@ prep_atts(Db, [{FileName, Data} | Rest]) -> {'DOWN', Ref, _, _, Resp} -> Resp after ?ATTACHMENT_WRITE_TIMEOUT -> - erlang:error(attachment_write_timeout) + error(attachment_write_timeout) end, [Att | prep_atts(Db, Rest)]. @@ -617,7 +617,7 @@ list_diff([T1 | R1], [T2 | R2]) -> compact(Db) -> {ok, Pid} = couch_db:start_compact(Db), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), % Ideally I'd assert that Pid is linked to us % at this point but its technically possible @@ -630,9 +630,9 @@ compact(Db) -> {'DOWN', Ref, _, _, noproc} -> ok; {'DOWN', Ref, _, _, Reason} -> - erlang:error({compactor_died, Reason}) + error({compactor_died, Reason}) after ?COMPACTOR_TIMEOUT -> - erlang:error(compactor_timed_out) + error(compactor_timed_out) end, test_util:wait(fun() -> diff --git a/src/couch_quickjs/.gitignore b/src/couch_quickjs/.gitignore index dd78afee12..f7d339351f 100644 --- a/src/couch_quickjs/.gitignore +++ b/src/couch_quickjs/.gitignore @@ -16,6 +16,8 @@ /quickjs/qjscalc.c /quickjs/repl.c /quickjs/run-test262 +/quickjs/test262_report.txt +/quickjs/test262/ /quickjs/test_fib.c /quickjs/.github compile_commands.json diff --git a/src/couch_quickjs/c_src/couchjs.c b/src/couch_quickjs/c_src/couchjs.c index 7958acda59..420f5f41d1 100644 --- a/src/couch_quickjs/c_src/couchjs.c +++ b/src/couch_quickjs/c_src/couchjs.c @@ -86,14 +86,20 @@ static CmdType parse_command(char* str, size_t len) { return CMD_VIEW; } -static void add_cx_methods(JSContext* cx) { +// Return true if initializations succeed and false if any fails. Failure +// will shortcut the sequence and will return early. The only thing to do +// then is to free the context and return early +// +static bool add_cx_methods(JSContext* cx) { //TODO: configure some features with env vars of command line switches - JS_AddIntrinsicBaseObjects(cx); - JS_AddIntrinsicEval(cx); - JS_AddIntrinsicJSON(cx); - JS_AddIntrinsicRegExp(cx); - JS_AddIntrinsicMapSet(cx); - JS_AddIntrinsicDate(cx); + return ! ( + JS_AddIntrinsicBaseObjects(cx) || + JS_AddIntrinsicEval(cx) || + JS_AddIntrinsicJSON(cx) || + JS_AddIntrinsicRegExp(cx) || + JS_AddIntrinsicMapSet(cx) || + JS_AddIntrinsicDate(cx) + ); } // Creates a new JSContext with only the provided sandbox function @@ -104,7 +110,12 @@ static JSContext* make_sandbox(JSContext* cx, JSValue sbox) { if(!cx1) { return NULL; } - add_cx_methods(cx1); + + if(!add_cx_methods(cx1)) { + JS_FreeContext(cx1); + return NULL; + } + JSValue global = JS_GetGlobalObject(cx1); int i; diff --git a/src/couch_quickjs/patches/01-spidermonkey-185-mode.patch b/src/couch_quickjs/patches/01-spidermonkey-185-mode.patch index 6b65fe4f6a..f50c02bf9a 100644 --- a/src/couch_quickjs/patches/01-spidermonkey-185-mode.patch +++ b/src/couch_quickjs/patches/01-spidermonkey-185-mode.patch @@ -1,6 +1,6 @@ ---- quickjs-master/quickjs.c 2025-04-30 08:12:10 -+++ quickjs/quickjs.c 2025-04-30 13:50:44 -@@ -30110,10 +30110,24 @@ +--- quickjs-master/quickjs.c 2025-11-05 05:46:20 ++++ quickjs/quickjs.c 2025-11-05 09:54:50 +@@ -31286,10 +31286,24 @@ if (s->token.val == TOK_FUNCTION || (token_is_pseudo_keyword(s, JS_ATOM_async) && peek_token(s, TRUE) == TOK_FUNCTION)) { diff --git a/src/couch_quickjs/patches/02-test262-errors.patch b/src/couch_quickjs/patches/02-test262-errors.patch new file mode 100644 index 0000000000..267bcb6c8b --- /dev/null +++ b/src/couch_quickjs/patches/02-test262-errors.patch @@ -0,0 +1,11 @@ +--- quickjs-master/test262_errors.txt 2025-11-05 05:46:20 ++++ quickjs/test262_errors.txt 2025-11-05 09:54:50 +@@ -19,6 +19,8 @@ + test262/test/language/expressions/compound-assignment/S11.13.2_A6.10_T1.js:24: Test262Error: #1: innerX === 2. Actual: 5 + test262/test/language/expressions/compound-assignment/S11.13.2_A6.11_T1.js:24: Test262Error: #1: innerX === 2. Actual: 5 + test262/test/language/identifier-resolution/assign-to-global-undefined.js:20: strict mode: expected error ++test262/test/language/statements/expression/S12.4_A1.js:15: unexpected error type: Test262: This statement should not be evaluated. ++test262/test/language/statements/expression/S12.4_A1.js:15: strict mode: unexpected error type: Test262: This statement should not be evaluated. + test262/test/staging/sm/Function/arguments-parameter-shadowing.js:14: Test262Error: Expected SameValue(«true», «false») to be true + test262/test/staging/sm/Function/constructor-binding.js:11: Test262Error: Expected SameValue(«"function"», «"undefined"») to be true + test262/test/staging/sm/Function/constructor-binding.js:11: strict mode: Test262Error: Expected SameValue(«"function"», «"undefined"») to be true diff --git a/src/couch_quickjs/quickjs/Changelog b/src/couch_quickjs/quickjs/Changelog index 7cc33993d9..7d6afd6da1 100644 --- a/src/couch_quickjs/quickjs/Changelog +++ b/src/couch_quickjs/quickjs/Changelog @@ -1,3 +1,31 @@ +- micro optimizations (15% faster on bench-v8) +- added resizable array buffers +- added ArrayBuffer.prototype.transfer +- added the Iterator object and methods +- added set methods +- added Atomics.pause +- added added Map and WeakMap upsert methods +- added Math.sumPrecise() +- misc bug fixes + +2025-09-13: + +- added JSON modules and import attributes +- added JS_PrintValue() API +- qjs: pretty print objects in print() and console.log() +- qjs: better promise rejection tracker heuristics +- added RegExp v flag +- added RegExp modifiers +- added RegExp.escape +- added Float16Array +- added Promise.try +- improved JSON parser spec conformance +- qjs: improved compatibility of std.parseExtJSON() with JSON5 and + accept JSON5 modules +- added JS_FreePropertyEnum() and JS_AtomToCStringLen() API +- added Error.isError() +- misc bug fixes + 2025-04-26: - removed the bignum extensions and qjscalc diff --git a/src/couch_quickjs/quickjs/Makefile b/src/couch_quickjs/quickjs/Makefile index 3b1c745947..ba64923d2b 100644 --- a/src/couch_quickjs/quickjs/Makefile +++ b/src/couch_quickjs/quickjs/Makefile @@ -54,6 +54,10 @@ PREFIX?=/usr/local # use UB sanitizer #CONFIG_UBSAN=y +# TEST262 bootstrap config: commit id and shallow "since" parameter +TEST262_COMMIT?=42303c7c2bcf1c1edb9e5375c291c6fbc8a261ab +TEST262_SINCE?=2025-09-01 + OBJDIR=.obj ifdef CONFIG_ASAN @@ -464,6 +468,15 @@ stats: qjs$(EXE) microbench: qjs$(EXE) $(WINE) ./qjs$(EXE) --std tests/microbench.js +ifeq ($(wildcard test262/features.txt),) +test2-bootstrap: + git clone --single-branch --shallow-since=$(TEST262_SINCE) https://github.com/tc39/test262.git + (cd test262 && git checkout -q $(TEST262_COMMIT) && patch -p1 < ../tests/test262.patch && cd ..) +else +test2-bootstrap: + (cd test262 && git fetch && git reset --hard $(TEST262_COMMIT) && patch -p1 < ../tests/test262.patch && cd ..) +endif + ifeq ($(wildcard test262o/tests.txt),) test2o test2o-update: @echo test262o tests not installed diff --git a/src/couch_quickjs/quickjs/VERSION b/src/couch_quickjs/quickjs/VERSION index c76e76d1f1..433b8f85bd 100644 --- a/src/couch_quickjs/quickjs/VERSION +++ b/src/couch_quickjs/quickjs/VERSION @@ -1 +1 @@ -2025-04-26 +2025-09-13 diff --git a/src/couch_quickjs/quickjs/cutils.c b/src/couch_quickjs/quickjs/cutils.c index c038cf44ca..52ff1649bd 100644 --- a/src/couch_quickjs/quickjs/cutils.c +++ b/src/couch_quickjs/quickjs/cutils.c @@ -100,15 +100,20 @@ void dbuf_init(DynBuf *s) dbuf_init2(s, NULL, NULL); } -/* return < 0 if error */ -int dbuf_realloc(DynBuf *s, size_t new_size) +/* Try to allocate 'len' more bytes. return < 0 if error */ +int dbuf_claim(DynBuf *s, size_t len) { - size_t size; + size_t new_size, size; uint8_t *new_buf; + new_size = s->size + len; + if (new_size < len) + return -1; /* overflow case */ if (new_size > s->allocated_size) { if (s->error) return -1; - size = s->allocated_size * 3 / 2; + size = s->allocated_size + (s->allocated_size / 2); + if (size < s->allocated_size) + return -1; /* overflow case */ if (size > new_size) new_size = size; new_buf = s->realloc_func(s->opaque, s->buf, new_size); @@ -122,22 +127,10 @@ int dbuf_realloc(DynBuf *s, size_t new_size) return 0; } -int dbuf_write(DynBuf *s, size_t offset, const uint8_t *data, size_t len) -{ - size_t end; - end = offset + len; - if (dbuf_realloc(s, end)) - return -1; - memcpy(s->buf + offset, data, len); - if (end > s->size) - s->size = end; - return 0; -} - int dbuf_put(DynBuf *s, const uint8_t *data, size_t len) { - if (unlikely((s->size + len) > s->allocated_size)) { - if (dbuf_realloc(s, s->size + len)) + if (unlikely((s->allocated_size - s->size) < len)) { + if (dbuf_claim(s, len)) return -1; } memcpy_no_ub(s->buf + s->size, data, len); @@ -147,8 +140,8 @@ int dbuf_put(DynBuf *s, const uint8_t *data, size_t len) int dbuf_put_self(DynBuf *s, size_t offset, size_t len) { - if (unlikely((s->size + len) > s->allocated_size)) { - if (dbuf_realloc(s, s->size + len)) + if (unlikely((s->allocated_size - s->size) < len)) { + if (dbuf_claim(s, len)) return -1; } memcpy(s->buf + s->size, s->buf + offset, len); @@ -156,11 +149,26 @@ int dbuf_put_self(DynBuf *s, size_t offset, size_t len) return 0; } -int dbuf_putc(DynBuf *s, uint8_t c) +int __dbuf_putc(DynBuf *s, uint8_t c) { return dbuf_put(s, &c, 1); } +int __dbuf_put_u16(DynBuf *s, uint16_t val) +{ + return dbuf_put(s, (uint8_t *)&val, 2); +} + +int __dbuf_put_u32(DynBuf *s, uint32_t val) +{ + return dbuf_put(s, (uint8_t *)&val, 4); +} + +int __dbuf_put_u64(DynBuf *s, uint64_t val) +{ + return dbuf_put(s, (uint8_t *)&val, 8); +} + int dbuf_putstr(DynBuf *s, const char *str) { return dbuf_put(s, (const uint8_t *)str, strlen(str)); @@ -182,7 +190,7 @@ int __attribute__((format(printf, 2, 3))) dbuf_printf(DynBuf *s, /* fast case */ return dbuf_put(s, (uint8_t *)buf, len); } else { - if (dbuf_realloc(s, s->size + len + 1)) + if (dbuf_claim(s, len + 1)) return -1; va_start(ap, fmt); vsnprintf((char *)(s->buf + s->size), s->allocated_size - s->size, diff --git a/src/couch_quickjs/quickjs/cutils.h b/src/couch_quickjs/quickjs/cutils.h index 32b97579db..094a8f1241 100644 --- a/src/couch_quickjs/quickjs/cutils.h +++ b/src/couch_quickjs/quickjs/cutils.h @@ -264,24 +264,58 @@ typedef struct DynBuf { void dbuf_init(DynBuf *s); void dbuf_init2(DynBuf *s, void *opaque, DynBufReallocFunc *realloc_func); -int dbuf_realloc(DynBuf *s, size_t new_size); -int dbuf_write(DynBuf *s, size_t offset, const uint8_t *data, size_t len); +int dbuf_claim(DynBuf *s, size_t len); int dbuf_put(DynBuf *s, const uint8_t *data, size_t len); int dbuf_put_self(DynBuf *s, size_t offset, size_t len); -int dbuf_putc(DynBuf *s, uint8_t c); int dbuf_putstr(DynBuf *s, const char *str); +int __dbuf_putc(DynBuf *s, uint8_t c); +int __dbuf_put_u16(DynBuf *s, uint16_t val); +int __dbuf_put_u32(DynBuf *s, uint32_t val); +int __dbuf_put_u64(DynBuf *s, uint64_t val); + +static inline int dbuf_putc(DynBuf *s, uint8_t val) +{ + if (unlikely((s->allocated_size - s->size) < 1)) { + return __dbuf_putc(s, val); + } else { + s->buf[s->size++] = val; + return 0; + } +} + static inline int dbuf_put_u16(DynBuf *s, uint16_t val) { - return dbuf_put(s, (uint8_t *)&val, 2); + if (unlikely((s->allocated_size - s->size) < 2)) { + return __dbuf_put_u16(s, val); + } else { + put_u16(s->buf + s->size, val); + s->size += 2; + return 0; + } } + static inline int dbuf_put_u32(DynBuf *s, uint32_t val) { - return dbuf_put(s, (uint8_t *)&val, 4); + if (unlikely((s->allocated_size - s->size) < 4)) { + return __dbuf_put_u32(s, val); + } else { + put_u32(s->buf + s->size, val); + s->size += 4; + return 0; + } } + static inline int dbuf_put_u64(DynBuf *s, uint64_t val) { - return dbuf_put(s, (uint8_t *)&val, 8); + if (unlikely((s->allocated_size - s->size) < 8)) { + return __dbuf_put_u64(s, val); + } else { + put_u64(s->buf + s->size, val); + s->size += 8; + return 0; + } } + int __attribute__((format(printf, 2, 3))) dbuf_printf(DynBuf *s, const char *fmt, ...); void dbuf_free(DynBuf *s); @@ -364,4 +398,60 @@ static inline double uint64_as_float64(uint64_t u64) return u.d; } +static inline double fromfp16(uint16_t v) +{ + double d; + uint32_t v1; + v1 = v & 0x7fff; + if (unlikely(v1 >= 0x7c00)) + v1 += 0x1f8000; /* NaN or infinity */ + d = uint64_as_float64(((uint64_t)(v >> 15) << 63) | ((uint64_t)v1 << (52 - 10))); + return d * 0x1p1008; +} + +static inline uint16_t tofp16(double d) +{ + uint64_t a, addend; + uint32_t v, sgn; + int shift; + + a = float64_as_uint64(d); + sgn = a >> 63; + a = a & 0x7fffffffffffffff; + if (unlikely(a > 0x7ff0000000000000)) { + /* nan */ + v = 0x7c01; + } else if (a < 0x3f10000000000000) { /* 0x1p-14 */ + /* subnormal f16 number or zero */ + if (a <= 0x3e60000000000000) { /* 0x1p-25 */ + v = 0x0000; /* zero */ + } else { + shift = 1051 - (a >> 52); + a = ((uint64_t)1 << 52) | (a & (((uint64_t)1 << 52) - 1)); + addend = ((a >> shift) & 1) + (((uint64_t)1 << (shift - 1)) - 1); + v = (a + addend) >> shift; + } + } else { + /* normal number or infinity */ + a -= 0x3f00000000000000; /* adjust the exponent */ + /* round */ + addend = ((a >> (52 - 10)) & 1) + (((uint64_t)1 << (52 - 11)) - 1); + v = (a + addend) >> (52 - 10); + /* overflow ? */ + if (unlikely(v > 0x7c00)) + v = 0x7c00; + } + return v | (sgn << 15); +} + +static inline int isfp16nan(uint16_t v) +{ + return (v & 0x7FFF) > 0x7C00; +} + +static inline int isfp16zero(uint16_t v) +{ + return (v & 0x7FFF) == 0; +} + #endif /* CUTILS_H */ diff --git a/src/couch_quickjs/quickjs/libregexp-opcode.h b/src/couch_quickjs/quickjs/libregexp-opcode.h index f255e09f27..ebab751dfc 100644 --- a/src/couch_quickjs/quickjs/libregexp-opcode.h +++ b/src/couch_quickjs/quickjs/libregexp-opcode.h @@ -26,11 +26,15 @@ DEF(invalid, 1) /* never used */ DEF(char, 3) +DEF(char_i, 3) DEF(char32, 5) +DEF(char32_i, 5) DEF(dot, 1) DEF(any, 1) /* same as dot but match any character including line terminator */ DEF(line_start, 1) +DEF(line_start_m, 1) DEF(line_end, 1) +DEF(line_end_m, 1) DEF(goto, 5) DEF(split_goto_first, 5) DEF(split_next_first, 5) @@ -42,11 +46,17 @@ DEF(loop, 5) /* decrement the top the stack and goto if != 0 */ DEF(push_i32, 5) /* push integer on the stack */ DEF(drop, 1) DEF(word_boundary, 1) +DEF(word_boundary_i, 1) DEF(not_word_boundary, 1) +DEF(not_word_boundary_i, 1) DEF(back_reference, 2) -DEF(backward_back_reference, 2) /* must come after back_reference */ +DEF(back_reference_i, 2) /* must come after */ +DEF(backward_back_reference, 2) /* must come after */ +DEF(backward_back_reference_i, 2) /* must come after */ DEF(range, 3) /* variable length */ +DEF(range_i, 3) /* variable length */ DEF(range32, 3) /* variable length */ +DEF(range32_i, 3) /* variable length */ DEF(lookahead, 5) DEF(negative_lookahead, 5) DEF(push_char_pos, 1) /* push the character position on the stack */ diff --git a/src/couch_quickjs/quickjs/libregexp.c b/src/couch_quickjs/quickjs/libregexp.c index 8c47389852..118d950eb9 100644 --- a/src/couch_quickjs/quickjs/libregexp.c +++ b/src/couch_quickjs/quickjs/libregexp.c @@ -71,7 +71,9 @@ typedef struct { const uint8_t *buf_start; int re_flags; BOOL is_unicode; + BOOL unicode_sets; /* if set, is_unicode is also set */ BOOL ignore_case; + BOOL multi_line; BOOL dotall; int capture_count; int total_capture_count; /* -1 = not computed yet */ @@ -102,11 +104,11 @@ static const REOpCode reopcode_info[REOP_COUNT] = { }; #define RE_HEADER_FLAGS 0 -#define RE_HEADER_CAPTURE_COUNT 1 -#define RE_HEADER_STACK_SIZE 2 -#define RE_HEADER_BYTECODE_LEN 3 +#define RE_HEADER_CAPTURE_COUNT 2 +#define RE_HEADER_STACK_SIZE 3 +#define RE_HEADER_BYTECODE_LEN 4 -#define RE_HEADER_LEN 7 +#define RE_HEADER_LEN 8 static inline int is_digit(int c) { return c >= '0' && c <= '9'; @@ -115,13 +117,271 @@ static inline int is_digit(int c) { /* insert 'len' bytes at position 'pos'. Return < 0 if error. */ static int dbuf_insert(DynBuf *s, int pos, int len) { - if (dbuf_realloc(s, s->size + len)) + if (dbuf_claim(s, len)) return -1; memmove(s->buf + pos + len, s->buf + pos, s->size - pos); s->size += len; return 0; } +typedef struct REString { + struct REString *next; + uint32_t hash; + uint32_t len; + uint32_t buf[]; +} REString; + +typedef struct { + /* the string list is the union of 'char_range' and of the strings + in hash_table[]. The strings in hash_table[] have a length != + 1. */ + CharRange cr; + uint32_t n_strings; + uint32_t hash_size; + int hash_bits; + REString **hash_table; +} REStringList; + +static uint32_t re_string_hash(int len, const uint32_t *buf) +{ + int i; + uint32_t h; + h = 1; + for(i = 0; i < len; i++) + h = h * 263 + buf[i]; + return h * 0x61C88647; +} + +static void re_string_list_init(REParseState *s1, REStringList *s) +{ + cr_init(&s->cr, s1->opaque, lre_realloc); + s->n_strings = 0; + s->hash_size = 0; + s->hash_bits = 0; + s->hash_table = NULL; +} + +static void re_string_list_free(REStringList *s) +{ + REString *p, *p_next; + int i; + for(i = 0; i < s->hash_size; i++) { + for(p = s->hash_table[i]; p != NULL; p = p_next) { + p_next = p->next; + lre_realloc(s->cr.mem_opaque, p, 0); + } + } + lre_realloc(s->cr.mem_opaque, s->hash_table, 0); + + cr_free(&s->cr); +} + +static void lre_print_char(int c, BOOL is_range) +{ + if (c == '\'' || c == '\\' || + (is_range && (c == '-' || c == ']'))) { + printf("\\%c", c); + } else if (c >= ' ' && c <= 126) { + printf("%c", c); + } else { + printf("\\u{%04x}", c); + } +} + +static __maybe_unused void re_string_list_dump(const char *str, const REStringList *s) +{ + REString *p; + const CharRange *cr; + int i, j, k; + + printf("%s:\n", str); + printf(" ranges: ["); + cr = &s->cr; + for(i = 0; i < cr->len; i += 2) { + lre_print_char(cr->points[i], TRUE); + if (cr->points[i] != cr->points[i + 1] - 1) { + printf("-"); + lre_print_char(cr->points[i + 1] - 1, TRUE); + } + } + printf("]\n"); + + j = 0; + for(i = 0; i < s->hash_size; i++) { + for(p = s->hash_table[i]; p != NULL; p = p->next) { + printf(" %d/%d: '", j, s->n_strings); + for(k = 0; k < p->len; k++) { + lre_print_char(p->buf[k], FALSE); + } + printf("'\n"); + j++; + } + } +} + +static int re_string_find2(REStringList *s, int len, const uint32_t *buf, + uint32_t h0, BOOL add_flag) +{ + uint32_t h = 0; /* avoid warning */ + REString *p; + if (s->n_strings != 0) { + h = h0 >> (32 - s->hash_bits); + for(p = s->hash_table[h]; p != NULL; p = p->next) { + if (p->hash == h0 && p->len == len && + !memcmp(p->buf, buf, len * sizeof(buf[0]))) { + return 1; + } + } + } + /* not found */ + if (!add_flag) + return 0; + /* increase the size of the hash table if needed */ + if (unlikely((s->n_strings + 1) > s->hash_size)) { + REString **new_hash_table, *p_next; + int new_hash_bits, i; + uint32_t new_hash_size; + new_hash_bits = max_int(s->hash_bits + 1, 4); + new_hash_size = 1 << new_hash_bits; + new_hash_table = lre_realloc(s->cr.mem_opaque, NULL, + sizeof(new_hash_table[0]) * new_hash_size); + if (!new_hash_table) + return -1; + memset(new_hash_table, 0, sizeof(new_hash_table[0]) * new_hash_size); + for(i = 0; i < s->hash_size; i++) { + for(p = s->hash_table[i]; p != NULL; p = p_next) { + p_next = p->next; + h = p->hash >> (32 - new_hash_bits); + p->next = new_hash_table[h]; + new_hash_table[h] = p; + } + } + lre_realloc(s->cr.mem_opaque, s->hash_table, 0); + s->hash_bits = new_hash_bits; + s->hash_size = new_hash_size; + s->hash_table = new_hash_table; + h = h0 >> (32 - s->hash_bits); + } + + p = lre_realloc(s->cr.mem_opaque, NULL, sizeof(REString) + len * sizeof(buf[0])); + if (!p) + return -1; + p->next = s->hash_table[h]; + s->hash_table[h] = p; + s->n_strings++; + p->hash = h0; + p->len = len; + memcpy(p->buf, buf, sizeof(buf[0]) * len); + return 1; +} + +static int re_string_find(REStringList *s, int len, const uint32_t *buf, + BOOL add_flag) +{ + uint32_t h0; + h0 = re_string_hash(len, buf); + return re_string_find2(s, len, buf, h0, add_flag); +} + +/* return -1 if memory error, 0 if OK */ +static int re_string_add(REStringList *s, int len, const uint32_t *buf) +{ + if (len == 1) { + return cr_union_interval(&s->cr, buf[0], buf[0]); + } + if (re_string_find(s, len, buf, TRUE) < 0) + return -1; + return 0; +} + +/* a = a op b */ +static int re_string_list_op(REStringList *a, REStringList *b, int op) +{ + int i, ret; + REString *p, **pp; + + if (cr_op1(&a->cr, b->cr.points, b->cr.len, op)) + return -1; + + switch(op) { + case CR_OP_UNION: + if (b->n_strings != 0) { + for(i = 0; i < b->hash_size; i++) { + for(p = b->hash_table[i]; p != NULL; p = p->next) { + if (re_string_find2(a, p->len, p->buf, p->hash, TRUE) < 0) + return -1; + } + } + } + break; + case CR_OP_INTER: + case CR_OP_SUB: + for(i = 0; i < a->hash_size; i++) { + pp = &a->hash_table[i]; + for(;;) { + p = *pp; + if (p == NULL) + break; + ret = re_string_find2(b, p->len, p->buf, p->hash, FALSE); + if (op == CR_OP_SUB) + ret = !ret; + if (!ret) { + /* remove it */ + *pp = p->next; + a->n_strings--; + lre_realloc(a->cr.mem_opaque, p, 0); + } else { + /* keep it */ + pp = &p->next; + } + } + } + break; + default: + abort(); + } + return 0; +} + +static int re_string_list_canonicalize(REParseState *s1, + REStringList *s, BOOL is_unicode) +{ + if (cr_regexp_canonicalize(&s->cr, is_unicode)) + return -1; + if (s->n_strings != 0) { + REStringList a_s, *a = &a_s; + int i, j; + REString *p; + + /* XXX: simplify */ + re_string_list_init(s1, a); + + a->n_strings = s->n_strings; + a->hash_size = s->hash_size; + a->hash_bits = s->hash_bits; + a->hash_table = s->hash_table; + + s->n_strings = 0; + s->hash_size = 0; + s->hash_bits = 0; + s->hash_table = NULL; + + for(i = 0; i < a->hash_size; i++) { + for(p = a->hash_table[i]; p != NULL; p = p->next) { + for(j = 0; j < p->len; j++) { + p->buf[j] = lre_canonicalize(p->buf[j], is_unicode); + } + if (re_string_add(s, p->len, p->buf)) { + re_string_list_free(a); + return -1; + } + } + } + re_string_list_free(a); + } + return 0; +} + static const uint16_t char_range_d[] = { 1, 0x0030, 0x0039 + 1, @@ -170,7 +430,7 @@ static const uint16_t * const char_range_table[] = { char_range_w, }; -static int cr_init_char_range(REParseState *s, CharRange *cr, uint32_t c) +static int cr_init_char_range(REParseState *s, REStringList *cr, uint32_t c) { BOOL invert; const uint16_t *c_pt; @@ -179,18 +439,18 @@ static int cr_init_char_range(REParseState *s, CharRange *cr, uint32_t c) invert = c & 1; c_pt = char_range_table[c >> 1]; len = *c_pt++; - cr_init(cr, s->opaque, lre_realloc); + re_string_list_init(s, cr); for(i = 0; i < len * 2; i++) { - if (cr_add_point(cr, c_pt[i])) + if (cr_add_point(&cr->cr, c_pt[i])) goto fail; } if (invert) { - if (cr_invert(cr)) + if (cr_invert(&cr->cr)) goto fail; } return 0; fail: - cr_free(cr); + re_string_list_free(cr); return -1; } @@ -240,6 +500,7 @@ static __maybe_unused void lre_dump_bytecode(const uint8_t *buf, printf("%s", reopcode_info[opcode].name); switch(opcode) { case REOP_char: + case REOP_char_i: val = get_u16(buf + pos + 1); if (val >= ' ' && val <= 126) printf(" '%c'", val); @@ -247,6 +508,7 @@ static __maybe_unused void lre_dump_bytecode(const uint8_t *buf, printf(" 0x%04x", val); break; case REOP_char32: + case REOP_char32_i: val = get_u32(buf + pos + 1); if (val >= ' ' && val <= 126) printf(" '%c'", val); @@ -273,7 +535,9 @@ static __maybe_unused void lre_dump_bytecode(const uint8_t *buf, case REOP_save_start: case REOP_save_end: case REOP_back_reference: + case REOP_back_reference_i: case REOP_backward_back_reference: + case REOP_backward_back_reference_i: printf(" %u", buf[pos + 1]); break; case REOP_save_reset: @@ -284,6 +548,7 @@ static __maybe_unused void lre_dump_bytecode(const uint8_t *buf, printf(" %d", val); break; case REOP_range: + case REOP_range_i: { int n, i; n = get_u16(buf + pos + 1); @@ -295,6 +560,7 @@ static __maybe_unused void lre_dump_bytecode(const uint8_t *buf, } break; case REOP_range32: + case REOP_range32_i: { int n, i; n = get_u16(buf + pos + 1); @@ -533,8 +799,16 @@ static BOOL is_unicode_char(int c) (c == '_')); } -static int parse_unicode_property(REParseState *s, CharRange *cr, - const uint8_t **pp, BOOL is_inv) +/* XXX: memory error test */ +static void seq_prop_cb(void *opaque, const uint32_t *seq, int seq_len) +{ + REStringList *sl = opaque; + re_string_add(sl, seq_len, seq); +} + +static int parse_unicode_property(REParseState *s, REStringList *cr, + const uint8_t **pp, BOOL is_inv, + BOOL allow_sequence_prop) { const uint8_t *p; char name[64], value[64]; @@ -574,51 +848,76 @@ static int parse_unicode_property(REParseState *s, CharRange *cr, } else if (!strcmp(name, "Script_Extensions") || !strcmp(name, "scx")) { script_ext = TRUE; do_script: - cr_init(cr, s->opaque, lre_realloc); - ret = unicode_script(cr, value, script_ext); + re_string_list_init(s, cr); + ret = unicode_script(&cr->cr, value, script_ext); if (ret) { - cr_free(cr); + re_string_list_free(cr); if (ret == -2) return re_parse_error(s, "unknown unicode script"); else goto out_of_memory; } } else if (!strcmp(name, "General_Category") || !strcmp(name, "gc")) { - cr_init(cr, s->opaque, lre_realloc); - ret = unicode_general_category(cr, value); + re_string_list_init(s, cr); + ret = unicode_general_category(&cr->cr, value); if (ret) { - cr_free(cr); + re_string_list_free(cr); if (ret == -2) return re_parse_error(s, "unknown unicode general category"); else goto out_of_memory; } } else if (value[0] == '\0') { - cr_init(cr, s->opaque, lre_realloc); - ret = unicode_general_category(cr, name); + re_string_list_init(s, cr); + ret = unicode_general_category(&cr->cr, name); if (ret == -1) { - cr_free(cr); + re_string_list_free(cr); goto out_of_memory; } if (ret < 0) { - ret = unicode_prop(cr, name); - if (ret) { - cr_free(cr); - if (ret == -2) - goto unknown_property_name; - else - goto out_of_memory; + ret = unicode_prop(&cr->cr, name); + if (ret == -1) { + re_string_list_free(cr); + goto out_of_memory; + } + } + if (ret < 0 && !is_inv && allow_sequence_prop) { + CharRange cr_tmp; + cr_init(&cr_tmp, s->opaque, lre_realloc); + ret = unicode_sequence_prop(name, seq_prop_cb, cr, &cr_tmp); + cr_free(&cr_tmp); + if (ret == -1) { + re_string_list_free(cr); + goto out_of_memory; } } + if (ret < 0) + goto unknown_property_name; } else { unknown_property_name: return re_parse_error(s, "unknown unicode property name"); } + /* the ordering of case folding and inversion differs with + unicode_sets. 'unicode_sets' ordering is more consistent */ + /* XXX: the spec seems incorrect, we do it as the other engines + seem to do it. */ + if (s->ignore_case && s->unicode_sets) { + if (re_string_list_canonicalize(s, cr, s->is_unicode)) { + re_string_list_free(cr); + goto out_of_memory; + } + } if (is_inv) { - if (cr_invert(cr)) { - cr_free(cr); - return -1; + if (cr_invert(&cr->cr)) { + re_string_list_free(cr); + goto out_of_memory; + } + } + if (s->ignore_case && !s->unicode_sets) { + if (re_string_list_canonicalize(s, cr, s->is_unicode)) { + re_string_list_free(cr); + goto out_of_memory; } } *pp = p; @@ -628,10 +927,61 @@ static int parse_unicode_property(REParseState *s, CharRange *cr, } #endif /* CONFIG_ALL_UNICODE */ +static int get_class_atom(REParseState *s, REStringList *cr, + const uint8_t **pp, BOOL inclass); + +static int parse_class_string_disjunction(REParseState *s, REStringList *cr, + const uint8_t **pp) +{ + const uint8_t *p; + DynBuf str; + int c; + + p = *pp; + if (*p != '{') + return re_parse_error(s, "expecting '{' after \\q"); + + dbuf_init2(&str, s->opaque, lre_realloc); + re_string_list_init(s, cr); + + p++; + for(;;) { + str.size = 0; + while (*p != '}' && *p != '|') { + c = get_class_atom(s, NULL, &p, FALSE); + if (c < 0) + goto fail; + if (dbuf_put_u32(&str, c)) { + re_parse_out_of_memory(s); + goto fail; + } + } + if (re_string_add(cr, str.size / 4, (uint32_t *)str.buf)) { + re_parse_out_of_memory(s); + goto fail; + } + if (*p == '}') + break; + p++; + } + if (s->ignore_case) { + if (re_string_list_canonicalize(s, cr, TRUE)) + goto fail; + } + p++; /* skip the '}' */ + dbuf_free(&str); + *pp = p; + return 0; + fail: + dbuf_free(&str); + re_string_list_free(cr); + return -1; +} + /* return -1 if error otherwise the character or a class range - (CLASS_RANGE_BASE). In case of class range, 'cr' is + (CLASS_RANGE_BASE) if cr != NULL. In case of class range, 'cr' is initialized. Otherwise, it is ignored. */ -static int get_class_atom(REParseState *s, CharRange *cr, +static int get_class_atom(REParseState *s, REStringList *cr, const uint8_t **pp, BOOL inclass) { const uint8_t *p; @@ -666,6 +1016,8 @@ static int get_class_atom(REParseState *s, CharRange *cr, case 'W': c = CHAR_RANGE_W; class_range: + if (!cr) + goto default_escape; if (cr_init_char_range(s, cr, c)) return -1; c = CLASS_RANGE_BASE; @@ -690,27 +1042,50 @@ static int get_class_atom(REParseState *s, CharRange *cr, if (!inclass && s->is_unicode) goto invalid_escape; break; + case '^': + case '$': + case '\\': + case '.': + case '*': + case '+': + case '?': + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + case '|': + case '/': + /* always valid to escape these characters */ + break; #ifdef CONFIG_ALL_UNICODE case 'p': case 'P': - if (s->is_unicode) { - if (parse_unicode_property(s, cr, &p, (c == 'P'))) + if (s->is_unicode && cr) { + if (parse_unicode_property(s, cr, &p, (c == 'P'), s->unicode_sets)) return -1; c = CLASS_RANGE_BASE; break; } - /* fall thru */ + goto default_escape; #endif + case 'q': + if (s->unicode_sets && cr && inclass) { + if (parse_class_string_disjunction(s, cr, &p)) + return -1; + c = CLASS_RANGE_BASE; + break; + } + goto default_escape; default: + default_escape: p--; ret = lre_parse_escape(&p, s->is_unicode * 2); if (ret >= 0) { c = ret; } else { - if (ret == -2 && *p != '\0' && strchr("^$\\.*+?()[]{}|/", *p)) { - /* always valid to escape these characters */ - goto normal_char; - } else if (s->is_unicode) { + if (s->is_unicode) { invalid_escape: return re_parse_error(s, "invalid escape sequence in regular expression"); } else { @@ -727,6 +1102,48 @@ static int get_class_atom(REParseState *s, CharRange *cr, return re_parse_error(s, "unexpected end"); } /* fall thru */ + goto normal_char; + + case '&': + case '!': + case '#': + case '$': + case '%': + case '*': + case '+': + case ',': + case '.': + case ':': + case ';': + case '<': + case '=': + case '>': + case '?': + case '@': + case '^': + case '`': + case '~': + if (s->unicode_sets && p[1] == c) { + /* forbidden double characters */ + return re_parse_error(s, "invalid class set operation in regular expression"); + } + goto normal_char; + + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + case '/': + case '-': + case '|': + if (s->unicode_sets) { + /* invalid characters in unicode sets */ + return re_parse_error(s, "invalid character in class in regular expression"); + } + goto normal_char; + default: normal_char: /* normal char */ @@ -754,8 +1171,6 @@ static int re_emit_range(REParseState *s, const CharRange *cr) if (len >= 65535) return re_parse_error(s, "too many ranges"); if (len == 0) { - /* not sure it can really happen. Emit a match that is always - false */ re_emit_op_u32(s, REOP_char32, -1); } else { high = cr->points[cr->len - 1]; @@ -764,7 +1179,7 @@ static int re_emit_range(REParseState *s, const CharRange *cr) if (high <= 0xffff) { /* can use 16 bit ranges with the conversion that 0xffff = infinity */ - re_emit_op_u16(s, REOP_range, len); + re_emit_op_u16(s, s->ignore_case ? REOP_range_i : REOP_range, len); for(i = 0; i < cr->len; i += 2) { dbuf_put_u16(&s->byte_code, cr->points[i]); high = cr->points[i + 1] - 1; @@ -773,7 +1188,7 @@ static int re_emit_range(REParseState *s, const CharRange *cr) dbuf_put_u16(&s->byte_code, high); } } else { - re_emit_op_u16(s, REOP_range32, len); + re_emit_op_u16(s, s->ignore_case ? REOP_range32_i : REOP_range32, len); for(i = 0; i < cr->len; i += 2) { dbuf_put_u32(&s->byte_code, cr->points[i]); dbuf_put_u32(&s->byte_code, cr->points[i + 1] - 1); @@ -783,15 +1198,139 @@ static int re_emit_range(REParseState *s, const CharRange *cr) return 0; } -static int re_parse_char_class(REParseState *s, const uint8_t **pp) +static int re_string_cmp_len(const void *a, const void *b, void *arg) +{ + REString *p1 = *(REString **)a; + REString *p2 = *(REString **)b; + return (p1->len < p2->len) - (p1->len > p2->len); +} + +static void re_emit_char(REParseState *s, int c) +{ + if (c <= 0xffff) + re_emit_op_u16(s, s->ignore_case ? REOP_char_i : REOP_char, c); + else + re_emit_op_u32(s, s->ignore_case ? REOP_char32_i : REOP_char32, c); +} + +static int re_emit_string_list(REParseState *s, const REStringList *sl) +{ + REString **tab, *p; + int i, j, split_pos, last_match_pos, n; + BOOL has_empty_string, is_last; + + // re_string_list_dump("sl", sl); + if (sl->n_strings == 0) { + /* simple case: only characters */ + if (re_emit_range(s, &sl->cr)) + return -1; + } else { + /* at least one string list is present : match the longest ones first */ + /* XXX: add a new op_switch opcode to compile as a trie */ + tab = lre_realloc(s->opaque, NULL, sizeof(tab[0]) * sl->n_strings); + if (!tab) { + re_parse_out_of_memory(s); + return -1; + } + has_empty_string = FALSE; + n = 0; + for(i = 0; i < sl->hash_size; i++) { + for(p = sl->hash_table[i]; p != NULL; p = p->next) { + if (p->len == 0) { + has_empty_string = TRUE; + } else { + tab[n++] = p; + } + } + } + assert(n <= sl->n_strings); + + rqsort(tab, n, sizeof(tab[0]), re_string_cmp_len, NULL); + + last_match_pos = -1; + for(i = 0; i < n; i++) { + p = tab[i]; + is_last = !has_empty_string && sl->cr.len == 0 && i == (n - 1); + if (!is_last) + split_pos = re_emit_op_u32(s, REOP_split_next_first, 0); + else + split_pos = 0; + for(j = 0; j < p->len; j++) { + re_emit_char(s, p->buf[j]); + } + if (!is_last) { + last_match_pos = re_emit_op_u32(s, REOP_goto, last_match_pos); + put_u32(s->byte_code.buf + split_pos, s->byte_code.size - (split_pos + 4)); + } + } + + if (sl->cr.len != 0) { + /* char range */ + is_last = !has_empty_string; + if (!is_last) + split_pos = re_emit_op_u32(s, REOP_split_next_first, 0); + else + split_pos = 0; /* not used */ + if (re_emit_range(s, &sl->cr)) { + lre_realloc(s->opaque, tab, 0); + return -1; + } + if (!is_last) + put_u32(s->byte_code.buf + split_pos, s->byte_code.size - (split_pos + 4)); + } + + /* patch the 'goto match' */ + while (last_match_pos != -1) { + int next_pos = get_u32(s->byte_code.buf + last_match_pos); + put_u32(s->byte_code.buf + last_match_pos, s->byte_code.size - (last_match_pos + 4)); + last_match_pos = next_pos; + } + + lre_realloc(s->opaque, tab, 0); + } + return 0; +} + +static int re_parse_nested_class(REParseState *s, REStringList *cr, const uint8_t **pp); + +static int re_parse_class_set_operand(REParseState *s, REStringList *cr, const uint8_t **pp) +{ + int c1; + const uint8_t *p = *pp; + + if (*p == '[') { + if (re_parse_nested_class(s, cr, pp)) + return -1; + } else { + c1 = get_class_atom(s, cr, pp, TRUE); + if (c1 < 0) + return -1; + if (c1 < CLASS_RANGE_BASE) { + /* create a range with a single character */ + re_string_list_init(s, cr); + if (s->ignore_case) + c1 = lre_canonicalize(c1, s->is_unicode); + if (cr_union_interval(&cr->cr, c1, c1)) { + re_string_list_free(cr); + return -1; + } + } + } + return 0; +} + +static int re_parse_nested_class(REParseState *s, REStringList *cr, const uint8_t **pp) { const uint8_t *p; uint32_t c1, c2; - CharRange cr_s, *cr = &cr_s; - CharRange cr1_s, *cr1 = &cr1_s; - BOOL invert; + int ret; + REStringList cr1_s, *cr1 = &cr1_s; + BOOL invert, is_first; - cr_init(cr, s->opaque, lre_realloc); + if (lre_check_stack_overflow(s->opaque, 0)) + return re_parse_error(s, "stack overflow"); + + re_string_list_init(s, cr); p = *pp; p++; /* skip '[' */ @@ -800,74 +1339,155 @@ static int re_parse_char_class(REParseState *s, const uint8_t **pp) p++; invert = TRUE; } - + + /* handle unions */ + is_first = TRUE; for(;;) { if (*p == ']') break; - c1 = get_class_atom(s, cr1, &p, TRUE); - if ((int)c1 < 0) - goto fail; - if (*p == '-' && p[1] != ']') { - const uint8_t *p0 = p + 1; - if (c1 >= CLASS_RANGE_BASE) { - if (s->is_unicode) { - cr_free(cr1); - goto invalid_class_range; - } - /* Annex B: match '-' character */ - goto class_atom; - } - c2 = get_class_atom(s, cr1, &p0, TRUE); - if ((int)c2 < 0) - goto fail; - if (c2 >= CLASS_RANGE_BASE) { - cr_free(cr1); - if (s->is_unicode) { - goto invalid_class_range; - } - /* Annex B: match '-' character */ - goto class_atom; - } - p = p0; - if (c2 < c1) { - invalid_class_range: - re_parse_error(s, "invalid class range"); + if (*p == '[' && s->unicode_sets) { + if (re_parse_nested_class(s, cr1, &p)) goto fail; - } - if (cr_union_interval(cr, c1, c2)) - goto memory_error; + goto class_union; } else { - class_atom: - if (c1 >= CLASS_RANGE_BASE) { - int ret; - ret = cr_union1(cr, cr1->points, cr1->len); - cr_free(cr1); - if (ret) - goto memory_error; + c1 = get_class_atom(s, cr1, &p, TRUE); + if ((int)c1 < 0) + goto fail; + if (*p == '-' && p[1] != ']') { + const uint8_t *p0 = p + 1; + if (p[1] == '-' && s->unicode_sets && is_first) + goto class_atom; /* first character class followed by '--' */ + if (c1 >= CLASS_RANGE_BASE) { + if (s->is_unicode) { + re_string_list_free(cr1); + goto invalid_class_range; + } + /* Annex B: match '-' character */ + goto class_atom; + } + c2 = get_class_atom(s, cr1, &p0, TRUE); + if ((int)c2 < 0) + goto fail; + if (c2 >= CLASS_RANGE_BASE) { + re_string_list_free(cr1); + if (s->is_unicode) { + goto invalid_class_range; + } + /* Annex B: match '-' character */ + goto class_atom; + } + p = p0; + if (c2 < c1) { + invalid_class_range: + re_parse_error(s, "invalid class range"); + goto fail; + } + if (s->ignore_case) { + CharRange cr2_s, *cr2 = &cr2_s; + cr_init(cr2, s->opaque, lre_realloc); + if (cr_add_interval(cr2, c1, c2 + 1) || + cr_regexp_canonicalize(cr2, s->is_unicode) || + cr_op1(&cr->cr, cr2->points, cr2->len, CR_OP_UNION)) { + cr_free(cr2); + goto memory_error; + } + cr_free(cr2); + } else { + if (cr_union_interval(&cr->cr, c1, c2)) + goto memory_error; + } + is_first = FALSE; /* union operation */ } else { - if (cr_union_interval(cr, c1, c1)) - goto memory_error; + class_atom: + if (c1 >= CLASS_RANGE_BASE) { + class_union: + ret = re_string_list_op(cr, cr1, CR_OP_UNION); + re_string_list_free(cr1); + if (ret) + goto memory_error; + } else { + if (s->ignore_case) + c1 = lre_canonicalize(c1, s->is_unicode); + if (cr_union_interval(&cr->cr, c1, c1)) + goto memory_error; + } } } + if (s->unicode_sets && is_first) { + if (*p == '&' && p[1] == '&' && p[2] != '&') { + /* handle '&&' */ + for(;;) { + if (*p == ']') { + break; + } else if (*p == '&' && p[1] == '&' && p[2] != '&') { + p += 2; + } else { + goto invalid_operation; + } + if (re_parse_class_set_operand(s, cr1, &p)) + goto fail; + ret = re_string_list_op(cr, cr1, CR_OP_INTER); + re_string_list_free(cr1); + if (ret) + goto memory_error; + } + } else if (*p == '-' && p[1] == '-') { + /* handle '--' */ + for(;;) { + if (*p == ']') { + break; + } else if (*p == '-' && p[1] == '-') { + p += 2; + } else { + invalid_operation: + re_parse_error(s, "invalid operation in regular expression"); + goto fail; + } + if (re_parse_class_set_operand(s, cr1, &p)) + goto fail; + ret = re_string_list_op(cr, cr1, CR_OP_SUB); + re_string_list_free(cr1); + if (ret) + goto memory_error; + } + } + } + is_first = FALSE; } - if (s->ignore_case) { - if (cr_regexp_canonicalize(cr, s->is_unicode)) - goto memory_error; - } + + p++; /* skip ']' */ + *pp = p; if (invert) { - if (cr_invert(cr)) + /* XXX: add may_contain_string syntax check to be fully + compliant. The test here accepts more input than the + spec. */ + if (cr->n_strings != 0) { + re_parse_error(s, "negated character class with strings in regular expression debugger eval code"); + goto fail; + } + if (cr_invert(&cr->cr)) goto memory_error; } - if (re_emit_range(s, cr)) - goto fail; - cr_free(cr); - p++; /* skip ']' */ - *pp = p; return 0; memory_error: re_parse_out_of_memory(s); fail: - cr_free(cr); + re_string_list_free(cr); + return -1; +} + +static int re_parse_char_class(REParseState *s, const uint8_t **pp) +{ + REStringList cr_s, *cr = &cr_s; + + if (re_parse_nested_class(s, cr, pp)) + return -1; + if (re_emit_string_list(s, cr)) + goto fail; + re_string_list_free(cr); + return 0; + fail: + re_string_list_free(cr); return -1; } @@ -888,27 +1508,35 @@ static BOOL re_need_check_advance(const uint8_t *bc_buf, int bc_buf_len) len = reopcode_info[opcode].size; switch(opcode) { case REOP_range: + case REOP_range_i: val = get_u16(bc_buf + pos + 1); len += val * 4; goto simple_char; case REOP_range32: + case REOP_range32_i: val = get_u16(bc_buf + pos + 1); len += val * 8; goto simple_char; case REOP_char: + case REOP_char_i: case REOP_char32: + case REOP_char32_i: case REOP_dot: case REOP_any: simple_char: ret = FALSE; break; case REOP_line_start: + case REOP_line_start_m: case REOP_line_end: + case REOP_line_end_m: case REOP_push_i32: case REOP_push_char_pos: case REOP_drop: case REOP_word_boundary: + case REOP_word_boundary_i: case REOP_not_word_boundary: + case REOP_not_word_boundary_i: case REOP_prev: /* no effect */ break; @@ -916,7 +1544,9 @@ static BOOL re_need_check_advance(const uint8_t *bc_buf, int bc_buf_len) case REOP_save_end: case REOP_save_reset: case REOP_back_reference: + case REOP_back_reference_i: case REOP_backward_back_reference: + case REOP_backward_back_reference_i: break; default: /* safe behavior: we cannot predict the outcome */ @@ -941,24 +1571,32 @@ static int re_is_simple_quantifier(const uint8_t *bc_buf, int bc_buf_len) len = reopcode_info[opcode].size; switch(opcode) { case REOP_range: + case REOP_range_i: val = get_u16(bc_buf + pos + 1); len += val * 4; goto simple_char; case REOP_range32: + case REOP_range32_i: val = get_u16(bc_buf + pos + 1); len += val * 8; goto simple_char; case REOP_char: + case REOP_char_i: case REOP_char32: + case REOP_char32_i: case REOP_dot: case REOP_any: simple_char: count++; break; case REOP_line_start: + case REOP_line_start_m: case REOP_line_end: + case REOP_line_end_m: case REOP_word_boundary: + case REOP_word_boundary_i: case REOP_not_word_boundary: + case REOP_not_word_boundary_i: break; default: return -1; @@ -1116,12 +1754,47 @@ static int find_group_name(REParseState *s, const char *name) static int re_parse_disjunction(REParseState *s, BOOL is_backward_dir); +static int re_parse_modifiers(REParseState *s, const uint8_t **pp) +{ + const uint8_t *p = *pp; + int mask = 0; + int val; + + for(;;) { + if (*p == 'i') { + val = LRE_FLAG_IGNORECASE; + } else if (*p == 'm') { + val = LRE_FLAG_MULTILINE; + } else if (*p == 's') { + val = LRE_FLAG_DOTALL; + } else { + break; + } + if (mask & val) + return re_parse_error(s, "duplicate modifier: '%c'", *p); + mask |= val; + p++; + } + *pp = p; + return mask; +} + +static BOOL update_modifier(BOOL val, int add_mask, int remove_mask, + int mask) +{ + if (add_mask & mask) + val = TRUE; + if (remove_mask & mask) + val = FALSE; + return val; +} + static int re_parse_term(REParseState *s, BOOL is_backward_dir) { const uint8_t *p; int c, last_atom_start, quant_min, quant_max, last_capture_count; BOOL greedy, add_zero_advance_check, is_neg, is_backward_lookahead; - CharRange cr_s, *cr = &cr_s; + REStringList cr_s, *cr = &cr_s; last_atom_start = -1; last_capture_count = 0; @@ -1130,11 +1803,11 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir) switch(c) { case '^': p++; - re_emit_op(s, REOP_line_start); + re_emit_op(s, s->multi_line ? REOP_line_start_m : REOP_line_start); break; case '$': p++; - re_emit_op(s, REOP_line_end); + re_emit_op(s, s->multi_line ? REOP_line_end_m : REOP_line_end); break; case '.': p++; @@ -1184,6 +1857,44 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir) p = s->buf_ptr; if (re_parse_expect(s, &p, ')')) return -1; + } else if (p[2] == 'i' || p[2] == 'm' || p[2] == 's' || p[2] == '-') { + BOOL saved_ignore_case, saved_multi_line, saved_dotall; + int add_mask, remove_mask; + p += 2; + remove_mask = 0; + add_mask = re_parse_modifiers(s, &p); + if (add_mask < 0) + return -1; + if (*p == '-') { + p++; + remove_mask = re_parse_modifiers(s, &p); + if (remove_mask < 0) + return -1; + } + if ((add_mask == 0 && remove_mask == 0) || + (add_mask & remove_mask) != 0) { + return re_parse_error(s, "invalid modifiers"); + } + if (re_parse_expect(s, &p, ':')) + return -1; + saved_ignore_case = s->ignore_case; + saved_multi_line = s->multi_line; + saved_dotall = s->dotall; + s->ignore_case = update_modifier(s->ignore_case, add_mask, remove_mask, LRE_FLAG_IGNORECASE); + s->multi_line = update_modifier(s->multi_line, add_mask, remove_mask, LRE_FLAG_MULTILINE); + s->dotall = update_modifier(s->dotall, add_mask, remove_mask, LRE_FLAG_DOTALL); + + last_atom_start = s->byte_code.size; + last_capture_count = s->capture_count; + s->buf_ptr = p; + if (re_parse_disjunction(s, is_backward_dir)) + return -1; + p = s->buf_ptr; + if (re_parse_expect(s, &p, ')')) + return -1; + s->ignore_case = saved_ignore_case; + s->multi_line = saved_multi_line; + s->dotall = saved_dotall; } else if ((p[2] == '=' || p[2] == '!')) { is_neg = (p[2] == '!'); is_backward_lookahead = FALSE; @@ -1262,7 +1973,11 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir) switch(p[1]) { case 'b': case 'B': - re_emit_op(s, REOP_word_boundary + (p[1] != 'b')); + if (p[1] != 'b') { + re_emit_op(s, s->ignore_case ? REOP_not_word_boundary_i : REOP_not_word_boundary); + } else { + re_emit_op(s, s->ignore_case ? REOP_word_boundary_i : REOP_word_boundary); + } p += 2; break; case 'k': @@ -1351,7 +2066,8 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir) emit_back_reference: last_atom_start = s->byte_code.size; last_capture_count = s->capture_count; - re_emit_op_u8(s, REOP_back_reference + is_backward_dir, c); + + re_emit_op_u8(s, REOP_back_reference + 2 * is_backward_dir + s->ignore_case, c); } break; default: @@ -1385,18 +2101,14 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir) re_emit_op(s, REOP_prev); if (c >= CLASS_RANGE_BASE) { int ret; - /* Note: canonicalization is not needed */ - ret = re_emit_range(s, cr); - cr_free(cr); + ret = re_emit_string_list(s, cr); + re_string_list_free(cr); if (ret) return -1; } else { if (s->ignore_case) c = lre_canonicalize(c, s->is_unicode); - if (c <= 0xffff) - re_emit_op_u16(s, REOP_char, c); - else - re_emit_op_u32(s, REOP_char32, c); + re_emit_char(s, c); } if (is_backward_dir) re_emit_op(s, REOP_prev); @@ -1628,7 +2340,7 @@ static int re_parse_alternative(REParseState *s, BOOL is_backward_dir) speed is not really critical here) */ end = s->byte_code.size; term_size = end - term_start; - if (dbuf_realloc(&s->byte_code, end + term_size)) + if (dbuf_claim(&s->byte_code, term_size)) return -1; memmove(s->byte_code.buf + start + term_size, s->byte_code.buf + start, @@ -1706,10 +2418,12 @@ static int compute_stack_size(const uint8_t *bc_buf, int bc_buf_len) stack_size--; break; case REOP_range: + case REOP_range_i: val = get_u16(bc_buf + pos + 1); len += val * 4; break; case REOP_range32: + case REOP_range32_i: val = get_u16(bc_buf + pos + 1); len += val * 8; break; @@ -1719,6 +2433,17 @@ static int compute_stack_size(const uint8_t *bc_buf, int bc_buf_len) return stack_size_max; } +static void *lre_bytecode_realloc(void *opaque, void *ptr, size_t size) +{ + if (size > (INT32_MAX / 2)) { + /* the bytecode cannot be larger than 2G. Leave some slack to + avoid some overflows. */ + return NULL; + } else { + return lre_realloc(opaque, ptr, size); + } +} + /* 'buf' must be a zero terminated UTF-8 string of length buf_len. Return NULL if error and allocate an error message in *perror_msg, otherwise the compiled bytecode and its length in plen. @@ -1737,18 +2462,20 @@ uint8_t *lre_compile(int *plen, char *error_msg, int error_msg_size, s->buf_end = s->buf_ptr + buf_len; s->buf_start = s->buf_ptr; s->re_flags = re_flags; - s->is_unicode = ((re_flags & LRE_FLAG_UNICODE) != 0); + s->is_unicode = ((re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0); is_sticky = ((re_flags & LRE_FLAG_STICKY) != 0); s->ignore_case = ((re_flags & LRE_FLAG_IGNORECASE) != 0); + s->multi_line = ((re_flags & LRE_FLAG_MULTILINE) != 0); s->dotall = ((re_flags & LRE_FLAG_DOTALL) != 0); + s->unicode_sets = ((re_flags & LRE_FLAG_UNICODE_SETS) != 0); s->capture_count = 1; s->total_capture_count = -1; s->has_named_captures = -1; - dbuf_init2(&s->byte_code, opaque, lre_realloc); + dbuf_init2(&s->byte_code, opaque, lre_bytecode_realloc); dbuf_init2(&s->group_names, opaque, lre_realloc); - dbuf_putc(&s->byte_code, re_flags); /* first element is the flags */ + dbuf_put_u16(&s->byte_code, re_flags); /* first element is the flags */ dbuf_putc(&s->byte_code, 0); /* second element is the number of captures */ dbuf_putc(&s->byte_code, 0); /* stack size */ dbuf_put_u32(&s->byte_code, 0); /* bytecode length */ @@ -1801,7 +2528,8 @@ uint8_t *lre_compile(int *plen, char *error_msg, int error_msg_size, /* add the named groups if needed */ if (s->group_names.size > (s->capture_count - 1)) { dbuf_put(&s->byte_code, s->group_names.buf, s->group_names.size); - s->byte_code.buf[RE_HEADER_FLAGS] |= LRE_FLAG_NAMED_GROUPS; + put_u16(s->byte_code.buf + RE_HEADER_FLAGS, + lre_get_flags(s->byte_code.buf) | LRE_FLAG_NAMED_GROUPS); } dbuf_free(&s->group_names); @@ -1935,8 +2663,6 @@ typedef struct { int cbuf_type; int capture_count; int stack_size_max; - BOOL multi_line; - BOOL ignore_case; BOOL is_unicode; int interrupt_counter; void *opaque; /* used for stack overflow check */ @@ -2085,17 +2811,19 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, } break; case REOP_char32: + case REOP_char32_i: val = get_u32(pc); pc += 4; goto test_char; case REOP_char: + case REOP_char_i: val = get_u16(pc); pc += 2; test_char: if (cptr >= cbuf_end) goto no_match; GET_CHAR(c, cptr, cbuf_end, cbuf_type); - if (s->ignore_case) { + if (opcode == REOP_char_i || opcode == REOP_char32_i) { c = lre_canonicalize(c, s->is_unicode); } if (val != c) @@ -2139,18 +2867,20 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, return LRE_RET_TIMEOUT; break; case REOP_line_start: + case REOP_line_start_m: if (cptr == s->cbuf) break; - if (!s->multi_line) + if (opcode == REOP_line_start) goto no_match; PEEK_PREV_CHAR(c, cptr, s->cbuf, cbuf_type); if (!is_line_terminator(c)) goto no_match; break; case REOP_line_end: + case REOP_line_end_m: if (cptr == cbuf_end) break; - if (!s->multi_line) + if (opcode == REOP_line_end) goto no_match; PEEK_CHAR(c, cptr, cbuf_end, cbuf_type); if (!is_line_terminator(c)) @@ -2213,14 +2943,20 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, goto no_match; break; case REOP_word_boundary: + case REOP_word_boundary_i: case REOP_not_word_boundary: + case REOP_not_word_boundary_i: { BOOL v1, v2; + int ignore_case = (opcode == REOP_word_boundary_i || opcode == REOP_not_word_boundary_i); + BOOL is_boundary = (opcode == REOP_word_boundary || opcode == REOP_word_boundary_i); /* char before */ if (cptr == s->cbuf) { v1 = FALSE; } else { PEEK_PREV_CHAR(c, cptr, s->cbuf, cbuf_type); + if (ignore_case) + c = lre_canonicalize(c, s->is_unicode); v1 = is_word_char(c); } /* current char */ @@ -2228,14 +2964,18 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, v2 = FALSE; } else { PEEK_CHAR(c, cptr, cbuf_end, cbuf_type); + if (ignore_case) + c = lre_canonicalize(c, s->is_unicode); v2 = is_word_char(c); } - if (v1 ^ v2 ^ (REOP_not_word_boundary - opcode)) + if (v1 ^ v2 ^ is_boundary) goto no_match; } break; case REOP_back_reference: + case REOP_back_reference_i: case REOP_backward_back_reference: + case REOP_backward_back_reference_i: { const uint8_t *cptr1, *cptr1_end, *cptr1_start; uint32_t c1, c2; @@ -2247,14 +2987,15 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, cptr1_end = capture[2 * val + 1]; if (!cptr1_start || !cptr1_end) break; - if (opcode == REOP_back_reference) { + if (opcode == REOP_back_reference || + opcode == REOP_back_reference_i) { cptr1 = cptr1_start; while (cptr1 < cptr1_end) { if (cptr >= cbuf_end) goto no_match; GET_CHAR(c1, cptr1, cptr1_end, cbuf_type); GET_CHAR(c2, cptr, cbuf_end, cbuf_type); - if (s->ignore_case) { + if (opcode == REOP_back_reference_i) { c1 = lre_canonicalize(c1, s->is_unicode); c2 = lre_canonicalize(c2, s->is_unicode); } @@ -2268,7 +3009,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, goto no_match; GET_PREV_CHAR(c1, cptr1, cptr1_start, cbuf_type); GET_PREV_CHAR(c2, cptr, s->cbuf, cbuf_type); - if (s->ignore_case) { + if (opcode == REOP_backward_back_reference_i) { c1 = lre_canonicalize(c1, s->is_unicode); c2 = lre_canonicalize(c2, s->is_unicode); } @@ -2279,6 +3020,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, } break; case REOP_range: + case REOP_range_i: { int n; uint32_t low, high, idx_min, idx_max, idx; @@ -2288,7 +3030,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, if (cptr >= cbuf_end) goto no_match; GET_CHAR(c, cptr, cbuf_end, cbuf_type); - if (s->ignore_case) { + if (opcode == REOP_range_i) { c = lre_canonicalize(c, s->is_unicode); } idx_min = 0; @@ -2319,6 +3061,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, } break; case REOP_range32: + case REOP_range32_i: { int n; uint32_t low, high, idx_min, idx_max, idx; @@ -2328,7 +3071,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, if (cptr >= cbuf_end) goto no_match; GET_CHAR(c, cptr, cbuf_end, cbuf_type); - if (s->ignore_case) { + if (opcode == REOP_range32_i) { c = lre_canonicalize(c, s->is_unicode); } idx_min = 0; @@ -2420,11 +3163,10 @@ int lre_exec(uint8_t **capture, REExecContext s_s, *s = &s_s; int re_flags, i, alloca_size, ret; StackInt *stack_buf; + const uint8_t *cptr; re_flags = lre_get_flags(bc_buf); - s->multi_line = (re_flags & LRE_FLAG_MULTILINE) != 0; - s->ignore_case = (re_flags & LRE_FLAG_IGNORECASE) != 0; - s->is_unicode = (re_flags & LRE_FLAG_UNICODE) != 0; + s->is_unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; s->capture_count = bc_buf[RE_HEADER_CAPTURE_COUNT]; s->stack_size_max = bc_buf[RE_HEADER_STACK_SIZE]; s->cbuf = cbuf; @@ -2446,8 +3188,17 @@ int lre_exec(uint8_t **capture, capture[i] = NULL; alloca_size = s->stack_size_max * sizeof(stack_buf[0]); stack_buf = alloca(alloca_size); + + cptr = cbuf + (cindex << cbuf_type); + if (0 < cindex && cindex < clen && s->cbuf_type == 2) { + const uint16_t *p = (const uint16_t *)cptr; + if (is_lo_surrogate(*p) && is_hi_surrogate(p[-1])) { + cptr = (const uint8_t *)(p - 1); + } + } + ret = lre_exec_backtrack(s, capture, stack_buf, 0, bc_buf + RE_HEADER_LEN, - cbuf + (cindex << cbuf_type), FALSE); + cptr, FALSE); lre_realloc(s->opaque, s->state_stack, 0); return ret; } @@ -2459,7 +3210,7 @@ int lre_get_capture_count(const uint8_t *bc_buf) int lre_get_flags(const uint8_t *bc_buf) { - return bc_buf[RE_HEADER_FLAGS]; + return get_u16(bc_buf + RE_HEADER_FLAGS); } /* Return NULL if no group names. Otherwise, return a pointer to diff --git a/src/couch_quickjs/quickjs/libregexp.h b/src/couch_quickjs/quickjs/libregexp.h index 7475bbea95..da76e4cef6 100644 --- a/src/couch_quickjs/quickjs/libregexp.h +++ b/src/couch_quickjs/quickjs/libregexp.h @@ -35,6 +35,7 @@ #define LRE_FLAG_STICKY (1 << 5) #define LRE_FLAG_INDICES (1 << 6) /* Unused by libregexp, just recorded. */ #define LRE_FLAG_NAMED_GROUPS (1 << 7) /* named groups are present in the regexp */ +#define LRE_FLAG_UNICODE_SETS (1 << 8) #define LRE_RET_MEMORY_ERROR (-1) #define LRE_RET_TIMEOUT (-2) diff --git a/src/couch_quickjs/quickjs/libunicode-table.h b/src/couch_quickjs/quickjs/libunicode-table.h index dc46f16df6..67df6b3a3c 100644 --- a/src/couch_quickjs/quickjs/libunicode-table.h +++ b/src/couch_quickjs/quickjs/libunicode-table.h @@ -3130,6 +3130,7 @@ typedef enum { } UnicodeScriptEnum; static const char unicode_script_name_table[] = + "Unknown,Zzzz" "\0" "Adlam,Adlm" "\0" "Ahom,Ahom" "\0" "Anatolian_Hieroglyphs,Hluw" "\0" @@ -4054,6 +4055,89 @@ static const uint8_t unicode_prop_Changes_When_NFKC_Casefolded1_table[450] = { 0x4f, 0xff, }; +static const uint8_t unicode_prop_Basic_Emoji1_table[143] = { + 0x60, 0x23, 0x19, 0x81, 0x40, 0xcc, 0x1a, 0x01, + 0x80, 0x42, 0x08, 0x81, 0x94, 0x81, 0xb1, 0x8b, + 0xaa, 0x80, 0x92, 0x80, 0x8c, 0x07, 0x81, 0x90, + 0x0c, 0x0f, 0x04, 0x80, 0x94, 0x06, 0x08, 0x03, + 0x01, 0x06, 0x03, 0x81, 0x9b, 0x80, 0xa2, 0x00, + 0x03, 0x10, 0x80, 0xbc, 0x82, 0x97, 0x80, 0x8d, + 0x80, 0x43, 0x5a, 0x81, 0xb2, 0x03, 0x80, 0x61, + 0xc4, 0xad, 0x80, 0x40, 0xc9, 0x80, 0x40, 0xbd, + 0x01, 0x89, 0xe5, 0x80, 0x97, 0x80, 0x93, 0x01, + 0x20, 0x82, 0x94, 0x81, 0x40, 0xad, 0xa0, 0x8b, + 0x88, 0x80, 0xc5, 0x80, 0x95, 0x8b, 0xaa, 0x1c, + 0x8b, 0x90, 0x10, 0x82, 0xc6, 0x00, 0x80, 0x40, + 0xba, 0x81, 0xbe, 0x8c, 0x18, 0x97, 0x91, 0x80, + 0x99, 0x81, 0x8c, 0x80, 0xd5, 0xd4, 0xaf, 0xc5, + 0x28, 0x12, 0x0a, 0x1b, 0x8a, 0x0e, 0x88, 0x40, + 0xe2, 0x8b, 0x18, 0x41, 0x1a, 0xae, 0x80, 0x89, + 0x80, 0x40, 0xb8, 0xef, 0x8c, 0x82, 0x89, 0x84, + 0xb7, 0x86, 0x8e, 0x81, 0x8a, 0x85, 0x88, +}; + +static const uint8_t unicode_prop_Basic_Emoji2_table[183] = { + 0x40, 0xa8, 0x03, 0x80, 0x5f, 0x8c, 0x80, 0x8b, + 0x80, 0x40, 0xd7, 0x80, 0x95, 0x80, 0xd9, 0x85, + 0x8e, 0x81, 0x41, 0x7c, 0x80, 0x40, 0xa5, 0x80, + 0x9c, 0x10, 0x0c, 0x82, 0x40, 0xc6, 0x80, 0x40, + 0xe6, 0x81, 0x89, 0x80, 0x88, 0x80, 0xb9, 0x0a, + 0x84, 0x88, 0x01, 0x05, 0x03, 0x01, 0x00, 0x09, + 0x02, 0x02, 0x0f, 0x14, 0x00, 0x80, 0x9b, 0x09, + 0x00, 0x08, 0x80, 0x91, 0x01, 0x80, 0x92, 0x00, + 0x18, 0x00, 0x0a, 0x05, 0x07, 0x81, 0x95, 0x05, + 0x00, 0x00, 0x80, 0x94, 0x05, 0x09, 0x01, 0x17, + 0x04, 0x09, 0x08, 0x01, 0x00, 0x00, 0x05, 0x02, + 0x80, 0x90, 0x81, 0x8e, 0x01, 0x80, 0x9a, 0x81, + 0xbb, 0x80, 0x41, 0x91, 0x81, 0x41, 0xce, 0x82, + 0x45, 0x27, 0x80, 0x8b, 0x80, 0x42, 0x58, 0x00, + 0x80, 0x61, 0xbe, 0xd5, 0x81, 0x8b, 0x81, 0x40, + 0x81, 0x80, 0xb3, 0x80, 0x40, 0xe8, 0x01, 0x88, + 0x88, 0x80, 0xc5, 0x80, 0x97, 0x08, 0x11, 0x81, + 0xaa, 0x1c, 0x8b, 0x92, 0x00, 0x00, 0x80, 0xc6, + 0x00, 0x80, 0x40, 0xba, 0x80, 0xca, 0x81, 0xa3, + 0x09, 0x86, 0x8c, 0x01, 0x19, 0x80, 0x93, 0x01, + 0x07, 0x81, 0x88, 0x04, 0x82, 0x8b, 0x17, 0x11, + 0x00, 0x03, 0x05, 0x02, 0x05, 0x80, 0x40, 0xcf, + 0x00, 0x82, 0x8f, 0x2a, 0x05, 0x01, 0x80, +}; + +static const uint8_t unicode_prop_RGI_Emoji_Modifier_Sequence_table[73] = { + 0x60, 0x26, 0x1c, 0x80, 0x40, 0xda, 0x80, 0x8f, + 0x83, 0x61, 0xcc, 0x76, 0x80, 0xbb, 0x11, 0x01, + 0x82, 0xf4, 0x09, 0x8a, 0x94, 0x18, 0x18, 0x88, + 0x10, 0x1a, 0x02, 0x30, 0x00, 0x97, 0x80, 0x40, + 0xc8, 0x0b, 0x80, 0x94, 0x03, 0x81, 0x40, 0xad, + 0x12, 0x84, 0xd2, 0x80, 0x8f, 0x82, 0x88, 0x80, + 0x8a, 0x80, 0x42, 0x3e, 0x01, 0x07, 0x3d, 0x80, + 0x88, 0x89, 0x11, 0xb7, 0x80, 0xbc, 0x08, 0x08, + 0x80, 0x90, 0x10, 0x8c, 0x40, 0xe4, 0x82, 0xa9, + 0x88, +}; + +static const uint8_t unicode_prop_RGI_Emoji_Flag_Sequence_table[128] = { + 0x0c, 0x00, 0x09, 0x00, 0x04, 0x01, 0x02, 0x06, + 0x03, 0x03, 0x01, 0x02, 0x01, 0x03, 0x07, 0x0d, + 0x18, 0x00, 0x09, 0x00, 0x00, 0x89, 0x08, 0x00, + 0x00, 0x81, 0x88, 0x83, 0x8c, 0x10, 0x00, 0x01, + 0x07, 0x08, 0x29, 0x10, 0x28, 0x00, 0x80, 0x8a, + 0x00, 0x0a, 0x00, 0x0e, 0x15, 0x18, 0x83, 0x89, + 0x06, 0x00, 0x81, 0x8d, 0x00, 0x12, 0x08, 0x00, + 0x03, 0x00, 0x24, 0x00, 0x05, 0x21, 0x00, 0x00, + 0x29, 0x90, 0x00, 0x02, 0x00, 0x08, 0x09, 0x00, + 0x08, 0x18, 0x8b, 0x80, 0x8c, 0x02, 0x19, 0x1a, + 0x11, 0x00, 0x00, 0x80, 0x9c, 0x80, 0x88, 0x02, + 0x00, 0x00, 0x02, 0x20, 0x88, 0x0a, 0x00, 0x03, + 0x01, 0x02, 0x05, 0x08, 0x00, 0x01, 0x09, 0x20, + 0x21, 0x18, 0x22, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x28, 0x89, 0x80, 0x8b, 0x80, 0x90, 0x80, 0x92, + 0x80, 0x8d, 0x05, 0x80, 0x8a, 0x80, 0x88, 0x80, +}; + +static const uint8_t unicode_prop_Emoji_Keycap_Sequence_table[4] = { + 0xa2, 0x05, 0x04, 0x89, +}; + static const uint8_t unicode_prop_ASCII_Hex_Digit_table[5] = { 0xaf, 0x89, 0x35, 0x99, 0x85, }; @@ -4493,6 +4577,11 @@ typedef enum { UNICODE_PROP_Changes_When_Titlecased1, UNICODE_PROP_Changes_When_Casefolded1, UNICODE_PROP_Changes_When_NFKC_Casefolded1, + UNICODE_PROP_Basic_Emoji1, + UNICODE_PROP_Basic_Emoji2, + UNICODE_PROP_RGI_Emoji_Modifier_Sequence, + UNICODE_PROP_RGI_Emoji_Flag_Sequence, + UNICODE_PROP_Emoji_Keycap_Sequence, UNICODE_PROP_ASCII_Hex_Digit, UNICODE_PROP_Bidi_Control, UNICODE_PROP_Dash, @@ -4633,6 +4722,11 @@ static const uint8_t * const unicode_prop_table[] = { unicode_prop_Changes_When_Titlecased1_table, unicode_prop_Changes_When_Casefolded1_table, unicode_prop_Changes_When_NFKC_Casefolded1_table, + unicode_prop_Basic_Emoji1_table, + unicode_prop_Basic_Emoji2_table, + unicode_prop_RGI_Emoji_Modifier_Sequence_table, + unicode_prop_RGI_Emoji_Flag_Sequence_table, + unicode_prop_Emoji_Keycap_Sequence_table, unicode_prop_ASCII_Hex_Digit_table, unicode_prop_Bidi_Control_table, unicode_prop_Dash_table, @@ -4688,6 +4782,11 @@ static const uint16_t unicode_prop_len_table[] = { countof(unicode_prop_Changes_When_Titlecased1_table), countof(unicode_prop_Changes_When_Casefolded1_table), countof(unicode_prop_Changes_When_NFKC_Casefolded1_table), + countof(unicode_prop_Basic_Emoji1_table), + countof(unicode_prop_Basic_Emoji2_table), + countof(unicode_prop_RGI_Emoji_Modifier_Sequence_table), + countof(unicode_prop_RGI_Emoji_Flag_Sequence_table), + countof(unicode_prop_Emoji_Keycap_Sequence_table), countof(unicode_prop_ASCII_Hex_Digit_table), countof(unicode_prop_Bidi_Control_table), countof(unicode_prop_Dash_table), @@ -4726,5 +4825,325 @@ static const uint16_t unicode_prop_len_table[] = { countof(unicode_prop_Case_Ignorable_table), }; +typedef enum { + UNICODE_SEQUENCE_PROP_Basic_Emoji, + UNICODE_SEQUENCE_PROP_Emoji_Keycap_Sequence, + UNICODE_SEQUENCE_PROP_RGI_Emoji_Modifier_Sequence, + UNICODE_SEQUENCE_PROP_RGI_Emoji_Flag_Sequence, + UNICODE_SEQUENCE_PROP_RGI_Emoji_Tag_Sequence, + UNICODE_SEQUENCE_PROP_RGI_Emoji_ZWJ_Sequence, + UNICODE_SEQUENCE_PROP_RGI_Emoji, + UNICODE_SEQUENCE_PROP_COUNT, +} UnicodeSequencePropertyEnum; + +static const char unicode_sequence_prop_name_table[] = + "Basic_Emoji" "\0" + "Emoji_Keycap_Sequence" "\0" + "RGI_Emoji_Modifier_Sequence" "\0" + "RGI_Emoji_Flag_Sequence" "\0" + "RGI_Emoji_Tag_Sequence" "\0" + "RGI_Emoji_ZWJ_Sequence" "\0" + "RGI_Emoji" "\0" +; + +static const uint8_t unicode_rgi_emoji_tag_sequence[18] = { + 0x67, 0x62, 0x65, 0x6e, 0x67, 0x00, 0x67, 0x62, + 0x73, 0x63, 0x74, 0x00, 0x67, 0x62, 0x77, 0x6c, + 0x73, 0x00, +}; + +static const uint8_t unicode_rgi_emoji_zwj_sequence[2320] = { + 0x02, 0xb8, 0x19, 0x40, 0x86, 0x02, 0xd1, 0x39, + 0xb0, 0x19, 0x02, 0x26, 0x39, 0x42, 0x86, 0x02, + 0xb4, 0x36, 0x42, 0x86, 0x03, 0x68, 0x54, 0x64, + 0x87, 0x68, 0x54, 0x02, 0xdc, 0x39, 0x42, 0x86, + 0x02, 0xd1, 0x39, 0x73, 0x13, 0x02, 0x39, 0x39, + 0x40, 0x86, 0x02, 0x69, 0x34, 0xbd, 0x19, 0x03, + 0xb6, 0x36, 0x40, 0x86, 0xa1, 0x87, 0x03, 0x68, + 0x74, 0x1d, 0x19, 0x68, 0x74, 0x03, 0x68, 0x34, + 0xbd, 0x19, 0xa1, 0x87, 0x02, 0xf1, 0x7a, 0xf2, + 0x7a, 0x02, 0xca, 0x33, 0x42, 0x86, 0x02, 0x69, + 0x34, 0xb0, 0x19, 0x04, 0x68, 0x14, 0x68, 0x14, + 0x67, 0x14, 0x66, 0x14, 0x02, 0xf9, 0x26, 0x42, + 0x86, 0x03, 0x69, 0x74, 0x1d, 0x19, 0x69, 0x74, + 0x03, 0xd1, 0x19, 0xbc, 0x19, 0xa1, 0x87, 0x02, + 0x3c, 0x19, 0x40, 0x86, 0x02, 0x68, 0x34, 0xeb, + 0x13, 0x02, 0xc3, 0x33, 0xa1, 0x87, 0x02, 0x70, + 0x34, 0x40, 0x86, 0x02, 0xd4, 0x39, 0x42, 0x86, + 0x02, 0xcf, 0x39, 0x42, 0x86, 0x02, 0x47, 0x36, + 0x40, 0x86, 0x02, 0x39, 0x39, 0x42, 0x86, 0x04, + 0xd1, 0x79, 0x64, 0x87, 0x8b, 0x14, 0xd1, 0x79, + 0x02, 0xd1, 0x39, 0x95, 0x86, 0x02, 0x68, 0x34, + 0x93, 0x13, 0x02, 0x69, 0x34, 0xed, 0x13, 0x02, + 0xda, 0x39, 0x40, 0x86, 0x03, 0x69, 0x34, 0xaf, + 0x19, 0xa1, 0x87, 0x02, 0xd1, 0x39, 0x93, 0x13, + 0x03, 0xce, 0x39, 0x42, 0x86, 0xa1, 0x87, 0x03, + 0xd1, 0x79, 0x64, 0x87, 0xd1, 0x79, 0x03, 0xc3, + 0x33, 0x42, 0x86, 0xa1, 0x87, 0x03, 0x69, 0x74, + 0x1d, 0x19, 0x68, 0x74, 0x02, 0x69, 0x34, 0x92, + 0x16, 0x02, 0xd1, 0x39, 0x96, 0x86, 0x04, 0x69, + 0x14, 0x64, 0x87, 0x8b, 0x14, 0x68, 0x14, 0x02, + 0x68, 0x34, 0x7c, 0x13, 0x02, 0x47, 0x36, 0x42, + 0x86, 0x02, 0x86, 0x34, 0x42, 0x86, 0x02, 0xd1, + 0x39, 0x7c, 0x13, 0x02, 0x69, 0x14, 0xa4, 0x13, + 0x02, 0xda, 0x39, 0x42, 0x86, 0x02, 0x37, 0x39, + 0x40, 0x86, 0x02, 0xd1, 0x39, 0x08, 0x87, 0x04, + 0x68, 0x54, 0x64, 0x87, 0x8b, 0x14, 0x68, 0x54, + 0x02, 0x4d, 0x36, 0x40, 0x86, 0x02, 0x68, 0x34, + 0x2c, 0x15, 0x02, 0x69, 0x34, 0xaf, 0x19, 0x02, + 0x6e, 0x34, 0x40, 0x86, 0x02, 0xcd, 0x39, 0x42, + 0x86, 0x02, 0xd1, 0x39, 0x2c, 0x15, 0x02, 0x6f, + 0x14, 0x40, 0x86, 0x03, 0xd1, 0x39, 0xbc, 0x19, + 0xa1, 0x87, 0x02, 0x68, 0x34, 0xa8, 0x13, 0x02, + 0x69, 0x34, 0x73, 0x13, 0x04, 0x69, 0x54, 0x64, + 0x87, 0x8b, 0x14, 0x68, 0x54, 0x02, 0x71, 0x34, + 0x42, 0x86, 0x02, 0xd1, 0x39, 0xa8, 0x13, 0x02, + 0x45, 0x36, 0x40, 0x86, 0x03, 0x69, 0x54, 0x64, + 0x87, 0x68, 0x54, 0x03, 0x69, 0x54, 0x64, 0x87, + 0x69, 0x54, 0x03, 0xce, 0x39, 0x40, 0x86, 0xa1, + 0x87, 0x02, 0xd8, 0x39, 0x40, 0x86, 0x03, 0xc3, + 0x33, 0x40, 0x86, 0xa1, 0x87, 0x02, 0x4d, 0x36, + 0x42, 0x86, 0x02, 0xd1, 0x19, 0x92, 0x16, 0x02, + 0xd1, 0x39, 0xeb, 0x13, 0x02, 0x68, 0x34, 0xbc, + 0x14, 0x02, 0xd1, 0x39, 0xbc, 0x14, 0x02, 0x3d, + 0x39, 0x40, 0x86, 0x02, 0xb8, 0x39, 0x42, 0x86, + 0x02, 0xa3, 0x36, 0x40, 0x86, 0x02, 0x75, 0x35, + 0x40, 0x86, 0x02, 0xd8, 0x39, 0x42, 0x86, 0x02, + 0x69, 0x34, 0x93, 0x13, 0x02, 0x35, 0x39, 0x40, + 0x86, 0x02, 0x4b, 0x36, 0x40, 0x86, 0x02, 0x3d, + 0x39, 0x42, 0x86, 0x02, 0x38, 0x39, 0x42, 0x86, + 0x02, 0xa3, 0x36, 0x42, 0x86, 0x03, 0x69, 0x14, + 0x67, 0x14, 0x67, 0x14, 0x02, 0xb6, 0x36, 0x40, + 0x86, 0x02, 0x69, 0x34, 0x7c, 0x13, 0x02, 0x75, + 0x35, 0x42, 0x86, 0x02, 0xcc, 0x93, 0x40, 0x86, + 0x02, 0xcc, 0x33, 0x40, 0x86, 0x03, 0xd1, 0x39, + 0xbd, 0x19, 0xa1, 0x87, 0x02, 0x82, 0x34, 0x40, + 0x86, 0x02, 0x87, 0x34, 0x40, 0x86, 0x02, 0x69, + 0x14, 0x3e, 0x13, 0x02, 0xd6, 0x39, 0x40, 0x86, + 0x02, 0x68, 0x14, 0xbd, 0x19, 0x02, 0x46, 0x36, + 0x42, 0x86, 0x02, 0x4b, 0x36, 0x42, 0x86, 0x02, + 0x69, 0x34, 0x2c, 0x15, 0x03, 0xb6, 0x36, 0x42, + 0x86, 0xa1, 0x87, 0x02, 0xc4, 0x33, 0x40, 0x86, + 0x02, 0x26, 0x19, 0x40, 0x86, 0x02, 0x69, 0x14, + 0xb0, 0x19, 0x02, 0xde, 0x19, 0x42, 0x86, 0x02, + 0x69, 0x34, 0xa8, 0x13, 0x02, 0xcc, 0x33, 0x42, + 0x86, 0x02, 0x82, 0x34, 0x42, 0x86, 0x02, 0xd1, + 0x19, 0x93, 0x13, 0x02, 0x81, 0x14, 0x42, 0x86, + 0x02, 0x69, 0x34, 0x95, 0x86, 0x02, 0x68, 0x34, + 0xbb, 0x14, 0x02, 0xd1, 0x39, 0xbb, 0x14, 0x02, + 0x69, 0x34, 0xeb, 0x13, 0x02, 0xd1, 0x39, 0x84, + 0x13, 0x02, 0x69, 0x34, 0xbc, 0x14, 0x04, 0x69, + 0x54, 0x64, 0x87, 0x8b, 0x14, 0x69, 0x54, 0x02, + 0x26, 0x39, 0x40, 0x86, 0x02, 0xb4, 0x36, 0x40, + 0x86, 0x02, 0x47, 0x16, 0x42, 0x86, 0x02, 0xdc, + 0x39, 0x40, 0x86, 0x02, 0xca, 0x33, 0x40, 0x86, + 0x02, 0xf9, 0x26, 0x40, 0x86, 0x02, 0x69, 0x34, + 0x08, 0x87, 0x03, 0x69, 0x14, 0x69, 0x14, 0x66, + 0x14, 0x03, 0xd1, 0x59, 0x1d, 0x19, 0xd1, 0x59, + 0x02, 0xd4, 0x39, 0x40, 0x86, 0x02, 0xcf, 0x39, + 0x40, 0x86, 0x02, 0x68, 0x34, 0xa4, 0x13, 0x02, + 0xd1, 0x39, 0xa4, 0x13, 0x02, 0xd1, 0x19, 0xa8, + 0x13, 0x02, 0xd7, 0x39, 0x42, 0x86, 0x03, 0x69, + 0x34, 0xbc, 0x19, 0xa1, 0x87, 0x02, 0x68, 0x14, + 0xb0, 0x19, 0x02, 0x68, 0x14, 0x73, 0x13, 0x04, + 0x69, 0x14, 0x69, 0x14, 0x66, 0x14, 0x66, 0x14, + 0x03, 0x68, 0x34, 0xaf, 0x19, 0xa1, 0x87, 0x02, + 0x68, 0x34, 0x80, 0x16, 0x02, 0x73, 0x34, 0x42, + 0x86, 0x02, 0xd1, 0x39, 0x80, 0x16, 0x02, 0x68, + 0x34, 0xb0, 0x19, 0x02, 0x86, 0x34, 0x40, 0x86, + 0x02, 0x38, 0x19, 0x42, 0x86, 0x02, 0x69, 0x34, + 0xbb, 0x14, 0x02, 0xb5, 0x36, 0x42, 0x86, 0x02, + 0xcd, 0x39, 0x40, 0x86, 0x02, 0x68, 0x34, 0x95, + 0x86, 0x02, 0x68, 0x34, 0x27, 0x15, 0x03, 0x68, + 0x14, 0x68, 0x14, 0x66, 0x14, 0x02, 0x71, 0x34, + 0x40, 0x86, 0x02, 0xd1, 0x39, 0x27, 0x15, 0x02, + 0x2e, 0x16, 0xa8, 0x14, 0x02, 0xc3, 0x33, 0x42, + 0x86, 0x02, 0x69, 0x14, 0x66, 0x14, 0x02, 0x68, + 0x34, 0x96, 0x86, 0x02, 0x69, 0x34, 0xa4, 0x13, + 0x03, 0x69, 0x14, 0x64, 0x87, 0x68, 0x14, 0x02, + 0xb8, 0x39, 0x40, 0x86, 0x02, 0x68, 0x34, 0x3e, + 0x13, 0x03, 0xd1, 0x19, 0xaf, 0x19, 0xa1, 0x87, + 0x02, 0xd1, 0x39, 0x3e, 0x13, 0x02, 0x68, 0x34, + 0xbd, 0x19, 0x02, 0xd1, 0x19, 0xbb, 0x14, 0x02, + 0xd1, 0x19, 0x95, 0x86, 0x02, 0xdb, 0x39, 0x42, + 0x86, 0x02, 0x38, 0x39, 0x40, 0x86, 0x02, 0x69, + 0x34, 0x80, 0x16, 0x02, 0x69, 0x14, 0xeb, 0x13, + 0x04, 0x68, 0x14, 0x69, 0x14, 0x67, 0x14, 0x67, + 0x14, 0x02, 0x77, 0x34, 0x42, 0x86, 0x02, 0x46, + 0x36, 0x40, 0x86, 0x02, 0x68, 0x34, 0x92, 0x16, + 0x02, 0x4e, 0x36, 0x42, 0x86, 0x03, 0x69, 0x14, + 0xbd, 0x19, 0xa1, 0x87, 0x02, 0xde, 0x19, 0x40, + 0x86, 0x02, 0x69, 0x34, 0x27, 0x15, 0x03, 0xc3, + 0x13, 0x40, 0x86, 0xa1, 0x87, 0x02, 0x81, 0x14, + 0x40, 0x86, 0x03, 0xd1, 0x39, 0xaf, 0x19, 0xa1, + 0x87, 0x02, 0x68, 0x34, 0xbc, 0x19, 0x02, 0xd1, + 0x19, 0x80, 0x16, 0x02, 0xd9, 0x39, 0x42, 0x86, + 0x02, 0xd1, 0x39, 0xbc, 0x19, 0x02, 0xdc, 0x19, + 0x42, 0x86, 0x02, 0x68, 0x34, 0x73, 0x13, 0x02, + 0x69, 0x34, 0x3e, 0x13, 0x02, 0x47, 0x16, 0x40, + 0x86, 0x02, 0xd1, 0x39, 0xbd, 0x19, 0x02, 0x3e, + 0x39, 0x42, 0x86, 0x02, 0x69, 0x14, 0x95, 0x86, + 0x02, 0x68, 0x14, 0x96, 0x86, 0x03, 0x69, 0x34, + 0xbd, 0x19, 0xa1, 0x87, 0x02, 0xd7, 0x39, 0x40, + 0x86, 0x02, 0x45, 0x16, 0x42, 0x86, 0x02, 0x68, + 0x34, 0xed, 0x13, 0x03, 0x68, 0x34, 0xbc, 0x19, + 0xa1, 0x87, 0x02, 0xd1, 0x39, 0xed, 0x13, 0x02, + 0xd1, 0x39, 0x92, 0x16, 0x02, 0x73, 0x34, 0x40, + 0x86, 0x02, 0x38, 0x19, 0x40, 0x86, 0x02, 0xb5, + 0x36, 0x40, 0x86, 0x02, 0x68, 0x34, 0xaf, 0x19, + 0x02, 0xd1, 0x39, 0xaf, 0x19, 0x02, 0x69, 0x34, + 0xbc, 0x19, 0x02, 0xb6, 0x16, 0x42, 0x86, 0x02, + 0x26, 0x14, 0x25, 0x15, 0x02, 0xc3, 0x33, 0x40, + 0x86, 0x02, 0xdd, 0x39, 0x42, 0x86, 0x02, 0xcb, + 0x93, 0x42, 0x86, 0x02, 0xcb, 0x33, 0x42, 0x86, + 0x02, 0x81, 0x34, 0x42, 0x86, 0x02, 0xce, 0x39, + 0xa1, 0x87, 0x02, 0xdb, 0x39, 0x40, 0x86, 0x02, + 0x68, 0x34, 0x08, 0x87, 0x02, 0xd1, 0x19, 0xb0, + 0x19, 0x02, 0x77, 0x34, 0x40, 0x86, 0x02, 0x4e, + 0x36, 0x40, 0x86, 0x02, 0xce, 0x39, 0x42, 0x86, + 0x02, 0x4e, 0x16, 0x42, 0x86, 0x02, 0xd9, 0x39, + 0x40, 0x86, 0x02, 0xdc, 0x19, 0x40, 0x86, 0x02, + 0x3e, 0x39, 0x40, 0x86, 0x02, 0xb9, 0x39, 0x42, + 0x86, 0x02, 0xda, 0x19, 0x42, 0x86, 0x02, 0x42, + 0x16, 0x94, 0x81, 0x02, 0x45, 0x16, 0x40, 0x86, + 0x02, 0x69, 0x14, 0xbd, 0x19, 0x02, 0x70, 0x34, + 0x42, 0x86, 0x02, 0xce, 0x19, 0xa1, 0x87, 0x02, + 0xc3, 0x13, 0x42, 0x86, 0x02, 0x68, 0x14, 0x08, + 0x87, 0x02, 0xd1, 0x19, 0x7c, 0x13, 0x02, 0x68, + 0x14, 0x92, 0x16, 0x02, 0xb6, 0x16, 0x40, 0x86, + 0x02, 0x37, 0x39, 0x42, 0x86, 0x03, 0xce, 0x19, + 0x42, 0x86, 0xa1, 0x87, 0x03, 0x68, 0x14, 0x67, + 0x14, 0x67, 0x14, 0x02, 0xdd, 0x39, 0x40, 0x86, + 0x02, 0xcf, 0x19, 0x42, 0x86, 0x02, 0xd1, 0x19, + 0x2c, 0x15, 0x02, 0x4b, 0x13, 0xe9, 0x17, 0x02, + 0x68, 0x14, 0x67, 0x14, 0x02, 0xcb, 0x93, 0x40, + 0x86, 0x02, 0x6e, 0x34, 0x42, 0x86, 0x02, 0xcb, + 0x33, 0x40, 0x86, 0x02, 0x81, 0x34, 0x40, 0x86, + 0x02, 0xb6, 0x36, 0xa1, 0x87, 0x02, 0x45, 0x36, + 0x42, 0x86, 0x02, 0xb4, 0x16, 0x42, 0x86, 0x02, + 0x69, 0x14, 0x73, 0x13, 0x04, 0x69, 0x14, 0x69, + 0x14, 0x67, 0x14, 0x66, 0x14, 0x02, 0x35, 0x39, + 0x42, 0x86, 0x02, 0x68, 0x14, 0x93, 0x13, 0x02, + 0xb6, 0x36, 0x42, 0x86, 0x03, 0x68, 0x14, 0x69, + 0x14, 0x66, 0x14, 0x02, 0xce, 0x39, 0x40, 0x86, + 0x02, 0x4e, 0x16, 0x40, 0x86, 0x02, 0x87, 0x34, + 0x42, 0x86, 0x02, 0x86, 0x14, 0x42, 0x86, 0x02, + 0xd6, 0x39, 0x42, 0x86, 0x02, 0xc4, 0x33, 0x42, + 0x86, 0x02, 0x69, 0x34, 0x96, 0x86, 0x02, 0xb9, + 0x39, 0x40, 0x86, 0x02, 0x68, 0x14, 0xa8, 0x13, + 0x02, 0xd1, 0x19, 0x84, 0x13, 0x02, 0xda, 0x19, + 0x40, 0x86, 0x02, 0xd8, 0x19, 0x42, 0x86, 0x02, + 0xc3, 0x13, 0x40, 0x86, 0x02, 0xb9, 0x19, 0x42, + 0x86, 0x02, 0x3d, 0x19, 0x42, 0x86, 0x02, 0xcf, + 0x19, 0x40, 0x86, 0x04, 0x68, 0x14, 0x68, 0x14, + 0x67, 0x14, 0x67, 0x14, 0x03, 0xd1, 0x19, 0xd1, + 0x19, 0xd2, 0x19, 0x02, 0x68, 0x14, 0xbb, 0x14, + 0x02, 0x3b, 0x14, 0x44, 0x87, 0x02, 0xd1, 0x19, + 0x27, 0x15, 0x02, 0xb4, 0x16, 0x40, 0x86, 0x02, + 0xcd, 0x19, 0x42, 0x86, 0x02, 0xd3, 0x86, 0xa5, + 0x14, 0x02, 0x70, 0x14, 0x42, 0x86, 0x03, 0xb6, + 0x16, 0x42, 0x86, 0xa1, 0x87, 0x04, 0x69, 0x14, + 0x64, 0x87, 0x8b, 0x14, 0x69, 0x14, 0x02, 0x36, + 0x16, 0x2b, 0x93, 0x02, 0x68, 0x14, 0x80, 0x16, + 0x02, 0x86, 0x14, 0x40, 0x86, 0x02, 0x08, 0x14, + 0x1b, 0x0b, 0x02, 0xd1, 0x19, 0xbc, 0x19, 0x02, + 0xca, 0x13, 0x42, 0x86, 0x02, 0x41, 0x94, 0xe8, + 0x95, 0x02, 0xd8, 0x19, 0x40, 0x86, 0x02, 0xb9, + 0x19, 0x40, 0x86, 0x02, 0xd1, 0x19, 0xed, 0x13, + 0x02, 0xf9, 0x86, 0x42, 0x86, 0x03, 0xd1, 0x19, + 0xbd, 0x19, 0xa1, 0x87, 0x02, 0x3d, 0x19, 0x40, + 0x86, 0x02, 0xd6, 0x19, 0x42, 0x86, 0x03, 0x69, + 0x14, 0x66, 0x14, 0x66, 0x14, 0x02, 0xd1, 0x19, + 0xaf, 0x19, 0x03, 0x69, 0x14, 0x69, 0x14, 0x67, + 0x14, 0x02, 0xcd, 0x19, 0x40, 0x86, 0x02, 0x70, + 0x14, 0x40, 0x86, 0x03, 0x68, 0x14, 0xbc, 0x19, + 0xa1, 0x87, 0x02, 0x6e, 0x14, 0x42, 0x86, 0x02, + 0x69, 0x14, 0x92, 0x16, 0x03, 0x68, 0x14, 0x68, + 0x14, 0x67, 0x14, 0x02, 0x69, 0x14, 0x67, 0x14, + 0x02, 0x75, 0x95, 0x42, 0x86, 0x03, 0x69, 0x14, + 0x64, 0x87, 0x69, 0x14, 0x02, 0xd1, 0x19, 0xbc, + 0x14, 0x02, 0xdf, 0x19, 0x42, 0x86, 0x02, 0xca, + 0x13, 0x40, 0x86, 0x02, 0x82, 0x14, 0x42, 0x86, + 0x02, 0x69, 0x14, 0x93, 0x13, 0x02, 0x68, 0x14, + 0x7c, 0x13, 0x02, 0xf9, 0x86, 0x40, 0x86, 0x02, + 0xd6, 0x19, 0x40, 0x86, 0x02, 0x68, 0x14, 0x2c, + 0x15, 0x02, 0x69, 0x14, 0xa8, 0x13, 0x02, 0xd4, + 0x19, 0x42, 0x86, 0x04, 0x68, 0x14, 0x69, 0x14, + 0x66, 0x14, 0x66, 0x14, 0x02, 0x77, 0x14, 0x42, + 0x86, 0x02, 0x39, 0x19, 0x42, 0x86, 0x02, 0xd1, + 0x19, 0xa4, 0x13, 0x02, 0x6e, 0x14, 0x40, 0x86, + 0x03, 0xd1, 0x19, 0xd2, 0x19, 0xd2, 0x19, 0x02, + 0x69, 0x14, 0xbb, 0x14, 0x02, 0xd1, 0x19, 0x96, + 0x86, 0x02, 0x75, 0x95, 0x40, 0x86, 0x04, 0x68, + 0x14, 0x64, 0x87, 0x8b, 0x14, 0x68, 0x14, 0x02, + 0xd1, 0x19, 0x3e, 0x13, 0x02, 0xdf, 0x19, 0x40, + 0x86, 0x02, 0x82, 0x14, 0x40, 0x86, 0x02, 0x44, + 0x13, 0xeb, 0x17, 0x02, 0xdd, 0x19, 0x42, 0x86, + 0x02, 0x69, 0x14, 0x80, 0x16, 0x03, 0x68, 0x14, + 0xaf, 0x19, 0xa1, 0x87, 0x02, 0xa3, 0x16, 0x42, + 0x86, 0x02, 0x69, 0x14, 0x96, 0x86, 0x02, 0x46, + 0x16, 0x42, 0x86, 0x02, 0xb6, 0x16, 0xa1, 0x87, + 0x02, 0x68, 0x14, 0x27, 0x15, 0x02, 0x26, 0x14, + 0x1b, 0x0b, 0x02, 0xd4, 0x19, 0x40, 0x86, 0x02, + 0x77, 0x14, 0x40, 0x86, 0x02, 0x39, 0x19, 0x40, + 0x86, 0x02, 0x37, 0x19, 0x42, 0x86, 0x03, 0x69, + 0x14, 0x67, 0x14, 0x66, 0x14, 0x03, 0xc3, 0x13, + 0x42, 0x86, 0xa1, 0x87, 0x02, 0x68, 0x14, 0xbc, + 0x19, 0x02, 0xd1, 0x19, 0xeb, 0x13, 0x04, 0x69, + 0x14, 0x69, 0x14, 0x67, 0x14, 0x67, 0x14, 0x02, + 0xd1, 0x19, 0x08, 0x87, 0x02, 0x68, 0x14, 0xed, + 0x13, 0x03, 0x69, 0x14, 0xbc, 0x19, 0xa1, 0x87, + 0x02, 0xdd, 0x19, 0x40, 0x86, 0x02, 0xc3, 0x13, + 0xa1, 0x87, 0x03, 0x68, 0x14, 0x66, 0x14, 0x66, + 0x14, 0x03, 0x68, 0x14, 0x69, 0x14, 0x67, 0x14, + 0x02, 0xa3, 0x16, 0x40, 0x86, 0x02, 0xdb, 0x19, + 0x42, 0x86, 0x02, 0x68, 0x14, 0xaf, 0x19, 0x02, + 0x46, 0x16, 0x40, 0x86, 0x02, 0x35, 0x16, 0xab, + 0x14, 0x02, 0x68, 0x14, 0x95, 0x86, 0x02, 0x42, + 0x16, 0x95, 0x81, 0x02, 0xc4, 0x13, 0x42, 0x86, + 0x02, 0x15, 0x14, 0xba, 0x19, 0x02, 0x69, 0x14, + 0x08, 0x87, 0x03, 0xd1, 0x19, 0x1d, 0x19, 0xd1, + 0x19, 0x02, 0x69, 0x14, 0x7c, 0x13, 0x02, 0x37, + 0x19, 0x40, 0x86, 0x02, 0x73, 0x14, 0x42, 0x86, + 0x02, 0x69, 0x14, 0x2c, 0x15, 0x02, 0xb5, 0x16, + 0x42, 0x86, 0x02, 0x35, 0x19, 0x42, 0x86, 0x04, + 0x68, 0x14, 0x69, 0x14, 0x67, 0x14, 0x66, 0x14, + 0x02, 0x64, 0x87, 0x25, 0x15, 0x02, 0x64, 0x87, + 0x79, 0x1a, 0x02, 0x68, 0x14, 0xbc, 0x14, 0x03, + 0xce, 0x19, 0x40, 0x86, 0xa1, 0x87, 0x02, 0x87, + 0x14, 0x42, 0x86, 0x02, 0x4d, 0x16, 0x42, 0x86, + 0x04, 0x68, 0x14, 0x68, 0x14, 0x66, 0x14, 0x66, + 0x14, 0x02, 0xdb, 0x19, 0x40, 0x86, 0x02, 0xd9, + 0x19, 0x42, 0x86, 0x02, 0xc4, 0x13, 0x40, 0x86, + 0x02, 0xd1, 0x19, 0xbd, 0x19, 0x02, 0x68, 0x14, + 0xa4, 0x13, 0x02, 0x3e, 0x19, 0x42, 0x86, 0x02, + 0xf3, 0x93, 0xa7, 0x86, 0x03, 0x69, 0x14, 0xaf, + 0x19, 0xa1, 0x87, 0x02, 0xf3, 0x93, 0x08, 0x13, + 0x02, 0xd1, 0x19, 0xd2, 0x19, 0x02, 0x73, 0x14, + 0x40, 0x86, 0x02, 0xb5, 0x16, 0x40, 0x86, 0x02, + 0x35, 0x19, 0x40, 0x86, 0x02, 0x69, 0x14, 0x27, + 0x15, 0x02, 0xce, 0x19, 0x42, 0x86, 0x02, 0x71, + 0x14, 0x42, 0x86, 0x02, 0xd1, 0x19, 0x73, 0x13, + 0x02, 0x68, 0x14, 0x3e, 0x13, 0x02, 0xf4, 0x13, + 0x20, 0x86, 0x02, 0x87, 0x14, 0x40, 0x86, 0x03, + 0xb6, 0x16, 0x40, 0x86, 0xa1, 0x87, 0x02, 0x4d, + 0x16, 0x40, 0x86, 0x02, 0x69, 0x14, 0xbc, 0x19, + 0x02, 0x4b, 0x16, 0x42, 0x86, 0x02, 0xd9, 0x19, + 0x40, 0x86, 0x02, 0x3e, 0x19, 0x40, 0x86, 0x02, + 0x69, 0x14, 0xed, 0x13, 0x02, 0xd7, 0x19, 0x42, + 0x86, 0x02, 0xb8, 0x19, 0x42, 0x86, 0x03, 0x68, + 0x14, 0x67, 0x14, 0x66, 0x14, 0x02, 0x3c, 0x19, + 0x42, 0x86, 0x02, 0x68, 0x14, 0x66, 0x14, 0x03, + 0x68, 0x14, 0x64, 0x87, 0x68, 0x14, 0x02, 0x69, + 0x14, 0xaf, 0x19, 0x02, 0xce, 0x19, 0x40, 0x86, + 0x02, 0x71, 0x14, 0x40, 0x86, 0x02, 0x68, 0x14, + 0xeb, 0x13, 0x03, 0x68, 0x14, 0xbd, 0x19, 0xa1, + 0x87, 0x02, 0x6f, 0x14, 0x42, 0x86, 0x04, 0xd1, + 0x19, 0xd1, 0x19, 0xd2, 0x19, 0xd2, 0x19, 0x02, + 0x69, 0x14, 0xbc, 0x14, 0x02, 0xcc, 0x93, 0x42, + 0x86, 0x02, 0x4b, 0x16, 0x40, 0x86, 0x02, 0x26, + 0x19, 0x42, 0x86, 0x02, 0xd7, 0x19, 0x40, 0x86, +}; + #endif /* CONFIG_ALL_UNICODE */ -/* 64 tables / 33442 bytes, 5 index / 351 bytes */ +/* 71 tables / 36311 bytes, 5 index / 351 bytes */ diff --git a/src/couch_quickjs/quickjs/libunicode.c b/src/couch_quickjs/quickjs/libunicode.c index d1bf1e91c1..0c510ccb15 100644 --- a/src/couch_quickjs/quickjs/libunicode.c +++ b/src/couch_quickjs/quickjs/libunicode.c @@ -499,6 +499,9 @@ int cr_op(CharRange *cr, const uint32_t *a_pt, int a_len, case CR_OP_XOR: is_in = (a_idx & 1) ^ (b_idx & 1); break; + case CR_OP_SUB: + is_in = (a_idx & 1) & ((b_idx & 1) ^ 1); + break; default: abort(); } @@ -511,14 +514,14 @@ int cr_op(CharRange *cr, const uint32_t *a_pt, int a_len, return 0; } -int cr_union1(CharRange *cr, const uint32_t *b_pt, int b_len) +int cr_op1(CharRange *cr, const uint32_t *b_pt, int b_len, int op) { CharRange a = *cr; int ret; cr->len = 0; cr->size = 0; cr->points = NULL; - ret = cr_op(cr, a.points, a.len, b_pt, b_len, CR_OP_UNION); + ret = cr_op(cr, a.points, a.len, b_pt, b_len, op); cr_free(&a); return ret; } @@ -1176,7 +1179,7 @@ int unicode_normalize(uint32_t **pdst, const uint32_t *src, int src_len, is_compat = n_type >> 1; dbuf_init2(dbuf, opaque, realloc_func); - if (dbuf_realloc(dbuf, sizeof(int) * src_len)) + if (dbuf_claim(dbuf, sizeof(int) * src_len)) goto fail; /* common case: latin1 is unaffected by NFC */ @@ -1282,8 +1285,6 @@ int unicode_script(CharRange *cr, script_idx = unicode_find_name(unicode_script_name_table, script_name); if (script_idx < 0) return -2; - /* Note: we remove the "Unknown" Script */ - script_idx += UNICODE_SCRIPT_Unknown + 1; is_common = (script_idx == UNICODE_SCRIPT_Common || script_idx == UNICODE_SCRIPT_Inherited); @@ -1313,17 +1314,21 @@ int unicode_script(CharRange *cr, n |= *p++; n += 96 + (1 << 12); } - if (type == 0) - v = 0; - else - v = *p++; c1 = c + n + 1; - if (v == script_idx) { - if (cr_add_interval(cr1, c, c1)) - goto fail; + if (type != 0) { + v = *p++; + if (v == script_idx || script_idx == UNICODE_SCRIPT_Unknown) { + if (cr_add_interval(cr1, c, c1)) + goto fail; + } } c = c1; } + if (script_idx == UNICODE_SCRIPT_Unknown) { + /* Unknown is all the characters outside scripts */ + if (cr_invert(cr1)) + goto fail; + } if (is_ext) { /* add the script extensions */ @@ -1554,6 +1559,7 @@ static int unicode_prop_ops(CharRange *cr, ...) cr2 = &stack[stack_len - 1]; cr3 = &stack[stack_len++]; cr_init(cr3, cr->mem_opaque, cr->realloc_func); + /* CR_OP_XOR may be used here */ if (cr_op(cr3, cr1->points, cr1->len, cr2->points, cr2->len, op - POP_UNION + CR_OP_UNION)) goto fail; @@ -1908,3 +1914,210 @@ BOOL lre_is_space_non_ascii(uint32_t c) } return FALSE; } + +#define SEQ_MAX_LEN 16 + +static int unicode_sequence_prop1(int seq_prop_idx, UnicodeSequencePropCB *cb, void *opaque, + CharRange *cr) +{ + int i, c, j; + uint32_t seq[SEQ_MAX_LEN]; + + switch(seq_prop_idx) { + case UNICODE_SEQUENCE_PROP_Basic_Emoji: + if (unicode_prop1(cr, UNICODE_PROP_Basic_Emoji1) < 0) + return -1; + for(i = 0; i < cr->len; i += 2) { + for(c = cr->points[i]; c < cr->points[i + 1]; c++) { + seq[0] = c; + cb(opaque, seq, 1); + } + } + + cr->len = 0; + + if (unicode_prop1(cr, UNICODE_PROP_Basic_Emoji2) < 0) + return -1; + for(i = 0; i < cr->len; i += 2) { + for(c = cr->points[i]; c < cr->points[i + 1]; c++) { + seq[0] = c; + seq[1] = 0xfe0f; + cb(opaque, seq, 2); + } + } + + break; + case UNICODE_SEQUENCE_PROP_RGI_Emoji_Modifier_Sequence: + if (unicode_prop1(cr, UNICODE_PROP_Emoji_Modifier_Base) < 0) + return -1; + for(i = 0; i < cr->len; i += 2) { + for(c = cr->points[i]; c < cr->points[i + 1]; c++) { + for(j = 0; j < 5; j++) { + seq[0] = c; + seq[1] = 0x1f3fb + j; + cb(opaque, seq, 2); + } + } + } + break; + case UNICODE_SEQUENCE_PROP_RGI_Emoji_Flag_Sequence: + if (unicode_prop1(cr, UNICODE_PROP_RGI_Emoji_Flag_Sequence) < 0) + return -1; + for(i = 0; i < cr->len; i += 2) { + for(c = cr->points[i]; c < cr->points[i + 1]; c++) { + int c0, c1; + c0 = c / 26; + c1 = c % 26; + seq[0] = 0x1F1E6 + c0; + seq[1] = 0x1F1E6 + c1; + cb(opaque, seq, 2); + } + } + break; + case UNICODE_SEQUENCE_PROP_RGI_Emoji_ZWJ_Sequence: + { + int len, code, pres, k, mod, mod_count, mod_pos[2], hc_pos, n_mod, n_hc, mod1; + int mod_idx, hc_idx, i0, i1; + const uint8_t *tab = unicode_rgi_emoji_zwj_sequence; + + for(i = 0; i < countof(unicode_rgi_emoji_zwj_sequence);) { + len = tab[i++]; + k = 0; + mod = 0; + mod_count = 0; + hc_pos = -1; + for(j = 0; j < len; j++) { + code = tab[i++]; + code |= tab[i++] << 8; + pres = code >> 15; + mod1 = (code >> 13) & 3; + code &= 0x1fff; + if (code < 0x1000) { + c = code + 0x2000; + } else { + c = 0x1f000 + (code - 0x1000); + } + if (c == 0x1f9b0) + hc_pos = k; + seq[k++] = c; + if (mod1 != 0) { + assert(mod_count < 2); + mod = mod1; + mod_pos[mod_count++] = k; + seq[k++] = 0; /* will be filled later */ + } + if (pres) { + seq[k++] = 0xfe0f; + } + if (j < len - 1) { + seq[k++] = 0x200d; + } + } + + /* genrate all the variants */ + switch(mod) { + case 1: + n_mod = 5; + break; + case 2: + n_mod = 25; + break; + case 3: + n_mod = 20; + break; + default: + n_mod = 1; + break; + } + if (hc_pos >= 0) + n_hc = 4; + else + n_hc = 1; + for(hc_idx = 0; hc_idx < n_hc; hc_idx++) { + for(mod_idx = 0; mod_idx < n_mod; mod_idx++) { + if (hc_pos >= 0) + seq[hc_pos] = 0x1f9b0 + hc_idx; + + switch(mod) { + case 1: + seq[mod_pos[0]] = 0x1f3fb + mod_idx; + break; + case 2: + case 3: + i0 = mod_idx / 5; + i1 = mod_idx % 5; + /* avoid identical values */ + if (mod == 3 && i0 >= i1) + i0++; + seq[mod_pos[0]] = 0x1f3fb + i0; + seq[mod_pos[1]] = 0x1f3fb + i1; + break; + default: + break; + } +#if 0 + for(j = 0; j < k; j++) + printf(" %04x", seq[j]); + printf("\n"); +#endif + cb(opaque, seq, k); + } + } + } + } + break; + case UNICODE_SEQUENCE_PROP_RGI_Emoji_Tag_Sequence: + { + for(i = 0; i < countof(unicode_rgi_emoji_tag_sequence);) { + j = 0; + seq[j++] = 0x1F3F4; + for(;;) { + c = unicode_rgi_emoji_tag_sequence[i++]; + if (c == 0x00) + break; + seq[j++] = 0xe0000 + c; + } + seq[j++] = 0xe007f; + cb(opaque, seq, j); + } + } + break; + case UNICODE_SEQUENCE_PROP_Emoji_Keycap_Sequence: + if (unicode_prop1(cr, UNICODE_PROP_Emoji_Keycap_Sequence) < 0) + return -1; + for(i = 0; i < cr->len; i += 2) { + for(c = cr->points[i]; c < cr->points[i + 1]; c++) { + seq[0] = c; + seq[1] = 0xfe0f; + seq[2] = 0x20e3; + cb(opaque, seq, 3); + } + } + break; + case UNICODE_SEQUENCE_PROP_RGI_Emoji: + /* all prevous sequences */ + for(i = UNICODE_SEQUENCE_PROP_Basic_Emoji; i <= UNICODE_SEQUENCE_PROP_RGI_Emoji_ZWJ_Sequence; i++) { + int ret; + ret = unicode_sequence_prop1(i, cb, opaque, cr); + if (ret < 0) + return ret; + cr->len = 0; + } + break; + default: + return -2; + } + return 0; +} + +/* build a unicode sequence property */ +/* return -2 if not found, -1 if other error. 'cr' is used as temporary memory. */ +int unicode_sequence_prop(const char *prop_name, UnicodeSequencePropCB *cb, void *opaque, + CharRange *cr) +{ + int seq_prop_idx; + seq_prop_idx = unicode_find_name(unicode_sequence_prop_name_table, prop_name); + if (seq_prop_idx < 0) + return -2; + return unicode_sequence_prop1(seq_prop_idx, cb, opaque, cr); +} diff --git a/src/couch_quickjs/quickjs/libunicode.h b/src/couch_quickjs/quickjs/libunicode.h index cc2f244c7f..5d964e40f7 100644 --- a/src/couch_quickjs/quickjs/libunicode.h +++ b/src/couch_quickjs/quickjs/libunicode.h @@ -45,6 +45,7 @@ typedef enum { CR_OP_UNION, CR_OP_INTER, CR_OP_XOR, + CR_OP_SUB, } CharRangeOpEnum; void cr_init(CharRange *cr, void *mem_opaque, void *(*realloc_func)(void *opaque, void *ptr, size_t size)); @@ -73,19 +74,18 @@ static inline int cr_add_interval(CharRange *cr, uint32_t c1, uint32_t c2) return 0; } -int cr_union1(CharRange *cr, const uint32_t *b_pt, int b_len); +int cr_op(CharRange *cr, const uint32_t *a_pt, int a_len, + const uint32_t *b_pt, int b_len, int op); +int cr_op1(CharRange *cr, const uint32_t *b_pt, int b_len, int op); static inline int cr_union_interval(CharRange *cr, uint32_t c1, uint32_t c2) { uint32_t b_pt[2]; b_pt[0] = c1; b_pt[1] = c2 + 1; - return cr_union1(cr, b_pt, 2); + return cr_op1(cr, b_pt, 2, CR_OP_UNION); } -int cr_op(CharRange *cr, const uint32_t *a_pt, int a_len, - const uint32_t *b_pt, int b_len, int op); - int cr_invert(CharRange *cr); int cr_regexp_canonicalize(CharRange *cr, int is_unicode); @@ -107,6 +107,10 @@ int unicode_script(CharRange *cr, const char *script_name, int is_ext); int unicode_general_category(CharRange *cr, const char *gc_name); int unicode_prop(CharRange *cr, const char *prop_name); +typedef void UnicodeSequencePropCB(void *opaque, const uint32_t *buf, int len); +int unicode_sequence_prop(const char *prop_name, UnicodeSequencePropCB *cb, void *opaque, + CharRange *cr); + int lre_case_conv(uint32_t *res, uint32_t c, int conv_type); int lre_canonicalize(uint32_t c, int is_unicode); diff --git a/src/couch_quickjs/quickjs/qjsc.c b/src/couch_quickjs/quickjs/qjsc.c index f9e1928a25..e55ca61ce6 100644 --- a/src/couch_quickjs/quickjs/qjsc.c +++ b/src/couch_quickjs/quickjs/qjsc.c @@ -170,14 +170,24 @@ static void dump_hex(FILE *f, const uint8_t *buf, size_t len) fprintf(f, "\n"); } +typedef enum { + CNAME_TYPE_SCRIPT, + CNAME_TYPE_MODULE, + CNAME_TYPE_JSON_MODULE, +} CNameTypeEnum; + static void output_object_code(JSContext *ctx, FILE *fo, JSValueConst obj, const char *c_name, - BOOL load_only) + CNameTypeEnum c_name_type) { uint8_t *out_buf; size_t out_buf_len; int flags; - flags = JS_WRITE_OBJ_BYTECODE; + + if (c_name_type == CNAME_TYPE_JSON_MODULE) + flags = 0; + else + flags = JS_WRITE_OBJ_BYTECODE; if (byte_swap) flags |= JS_WRITE_OBJ_BSWAP; out_buf = JS_WriteObject(ctx, &out_buf_len, obj, flags); @@ -186,7 +196,7 @@ static void output_object_code(JSContext *ctx, exit(1); } - namelist_add(&cname_list, c_name, NULL, load_only); + namelist_add(&cname_list, c_name, NULL, c_name_type); fprintf(fo, "const uint32_t %s_size = %u;\n\n", c_name, (unsigned int)out_buf_len); @@ -227,7 +237,8 @@ static void find_unique_cname(char *cname, size_t cname_size) } JSModuleDef *jsc_module_loader(JSContext *ctx, - const char *module_name, void *opaque) + const char *module_name, void *opaque, + JSValueConst attributes) { JSModuleDef *m; namelist_entry_t *e; @@ -249,9 +260,9 @@ JSModuleDef *jsc_module_loader(JSContext *ctx, } else { size_t buf_len; uint8_t *buf; - JSValue func_val; char cname[1024]; - + int res; + buf = js_load_file(ctx, &buf_len, module_name); if (!buf) { JS_ThrowReferenceError(ctx, "could not load module filename '%s'", @@ -259,21 +270,59 @@ JSModuleDef *jsc_module_loader(JSContext *ctx, return NULL; } - /* compile the module */ - func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name, - JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); - js_free(ctx, buf); - if (JS_IsException(func_val)) - return NULL; - get_c_name(cname, sizeof(cname), module_name); - if (namelist_find(&cname_list, cname)) { - find_unique_cname(cname, sizeof(cname)); - } - output_object_code(ctx, outfile, func_val, cname, TRUE); + res = js_module_test_json(ctx, attributes); + if (has_suffix(module_name, ".json") || res > 0) { + /* compile as JSON or JSON5 depending on "type" */ + JSValue val; + int flags; + + if (res == 2) + flags = JS_PARSE_JSON_EXT; + else + flags = 0; + val = JS_ParseJSON2(ctx, (char *)buf, buf_len, module_name, flags); + js_free(ctx, buf); + if (JS_IsException(val)) + return NULL; + /* create a dummy module */ + m = JS_NewCModule(ctx, module_name, js_module_dummy_init); + if (!m) { + JS_FreeValue(ctx, val); + return NULL; + } - /* the module is already referenced, so we must free it */ - m = JS_VALUE_GET_PTR(func_val); - JS_FreeValue(ctx, func_val); + get_c_name(cname, sizeof(cname), module_name); + if (namelist_find(&cname_list, cname)) { + find_unique_cname(cname, sizeof(cname)); + } + + /* output the module name */ + fprintf(outfile, "static const uint8_t %s_module_name[] = {\n", + cname); + dump_hex(outfile, (const uint8_t *)module_name, strlen(module_name) + 1); + fprintf(outfile, "};\n\n"); + + output_object_code(ctx, outfile, val, cname, CNAME_TYPE_JSON_MODULE); + JS_FreeValue(ctx, val); + } else { + JSValue func_val; + + /* compile the module */ + func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name, + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + js_free(ctx, buf); + if (JS_IsException(func_val)) + return NULL; + get_c_name(cname, sizeof(cname), module_name); + if (namelist_find(&cname_list, cname)) { + find_unique_cname(cname, sizeof(cname)); + } + output_object_code(ctx, outfile, func_val, cname, CNAME_TYPE_MODULE); + + /* the module is already referenced, so we must free it */ + m = JS_VALUE_GET_PTR(func_val); + JS_FreeValue(ctx, func_val); + } } return m; } @@ -313,8 +362,11 @@ static void compile_file(JSContext *ctx, FILE *fo, pstrcpy(c_name, sizeof(c_name), c_name1); } else { get_c_name(c_name, sizeof(c_name), filename); + if (namelist_find(&cname_list, c_name)) { + find_unique_cname(c_name, sizeof(c_name)); + } } - output_object_code(ctx, fo, obj, c_name, FALSE); + output_object_code(ctx, fo, obj, c_name, CNAME_TYPE_SCRIPT); JS_FreeValue(ctx, obj); } @@ -709,7 +761,7 @@ int main(int argc, char **argv) JS_SetStripInfo(rt, strip_flags); /* loader for ES6 modules */ - JS_SetModuleLoaderFunc(rt, NULL, jsc_module_loader, NULL); + JS_SetModuleLoaderFunc2(rt, NULL, jsc_module_loader, NULL, NULL); fprintf(fo, "/* File generated automatically by the QuickJS compiler. */\n" "\n" @@ -732,7 +784,7 @@ int main(int argc, char **argv) } for(i = 0; i < dynamic_module_list.count; i++) { - if (!jsc_module_loader(ctx, dynamic_module_list.array[i].name, NULL)) { + if (!jsc_module_loader(ctx, dynamic_module_list.array[i].name, NULL, JS_UNDEFINED)) { fprintf(stderr, "Could not load dynamic module '%s'\n", dynamic_module_list.array[i].name); exit(1); @@ -770,9 +822,12 @@ int main(int argc, char **argv) } for(i = 0; i < cname_list.count; i++) { namelist_entry_t *e = &cname_list.array[i]; - if (e->flags) { + if (e->flags == CNAME_TYPE_MODULE) { fprintf(fo, " js_std_eval_binary(ctx, %s, %s_size, 1);\n", e->name, e->name); + } else if (e->flags == CNAME_TYPE_JSON_MODULE) { + fprintf(fo, " js_std_eval_binary_json_module(ctx, %s, %s_size, (const char *)%s_module_name);\n", + e->name, e->name, e->name); } } fprintf(fo, @@ -788,7 +843,7 @@ int main(int argc, char **argv) /* add the module loader if necessary */ if (feature_bitmap & (1 << FE_MODULE_LOADER)) { - fprintf(fo, " JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL);\n"); + fprintf(fo, " JS_SetModuleLoaderFunc2(rt, NULL, js_module_loader, js_module_check_attributes, NULL);\n"); } fprintf(fo, @@ -797,7 +852,7 @@ int main(int argc, char **argv) for(i = 0; i < cname_list.count; i++) { namelist_entry_t *e = &cname_list.array[i]; - if (!e->flags) { + if (e->flags == CNAME_TYPE_SCRIPT) { fprintf(fo, " js_std_eval_binary(ctx, %s, %s_size, 0);\n", e->name, e->name); } diff --git a/src/couch_quickjs/quickjs/quickjs-atom.h b/src/couch_quickjs/quickjs/quickjs-atom.h index 73766f23c7..23c2ed07a2 100644 --- a/src/couch_quickjs/quickjs/quickjs-atom.h +++ b/src/couch_quickjs/quickjs/quickjs-atom.h @@ -78,6 +78,8 @@ DEF(await, "await") /* empty string */ DEF(empty_string, "") /* identifiers */ +DEF(keys, "keys") +DEF(size, "size") DEF(length, "length") DEF(fileName, "fileName") DEF(lineNumber, "lineNumber") @@ -177,12 +179,19 @@ DEF(minus_zero, "-0") DEF(Infinity, "Infinity") DEF(minus_Infinity, "-Infinity") DEF(NaN, "NaN") +DEF(hasIndices, "hasIndices") +DEF(ignoreCase, "ignoreCase") +DEF(multiline, "multiline") +DEF(dotAll, "dotAll") +DEF(sticky, "sticky") +DEF(unicodeSets, "unicodeSets") /* the following 3 atoms are only used with CONFIG_ATOMICS */ DEF(not_equal, "not-equal") DEF(timed_out, "timed-out") DEF(ok, "ok") /* */ DEF(toJSON, "toJSON") +DEF(maxByteLength, "maxByteLength") /* class names */ DEF(Object, "Object") DEF(Array, "Array") @@ -211,6 +220,7 @@ DEF(Int32Array, "Int32Array") DEF(Uint32Array, "Uint32Array") DEF(BigInt64Array, "BigInt64Array") DEF(BigUint64Array, "BigUint64Array") +DEF(Float16Array, "Float16Array") DEF(Float32Array, "Float32Array") DEF(Float64Array, "Float64Array") DEF(DataView, "DataView") @@ -221,6 +231,9 @@ DEF(Map, "Map") DEF(Set, "Set") /* Map + 1 */ DEF(WeakMap, "WeakMap") /* Map + 2 */ DEF(WeakSet, "WeakSet") /* Map + 3 */ +DEF(Iterator, "Iterator") +DEF(IteratorHelper, "Iterator Helper") +DEF(IteratorWrap, "Iterator Wrap") DEF(Map_Iterator, "Map Iterator") DEF(Set_Iterator, "Set Iterator") DEF(Array_Iterator, "Array Iterator") @@ -243,6 +256,7 @@ DEF(SyntaxError, "SyntaxError") DEF(TypeError, "TypeError") DEF(URIError, "URIError") DEF(InternalError, "InternalError") +DEF(AggregateError, "AggregateError") /* private symbols */ DEF(Private_brand, "") /* symbols */ diff --git a/src/couch_quickjs/quickjs/quickjs-libc.c b/src/couch_quickjs/quickjs/quickjs-libc.c index b59c6ac55b..54a7a15bd5 100644 --- a/src/couch_quickjs/quickjs/quickjs-libc.c +++ b/src/couch_quickjs/quickjs/quickjs-libc.c @@ -136,11 +136,18 @@ typedef struct { JSValue on_message_func; } JSWorkerMessageHandler; +typedef struct { + struct list_head link; + JSValue promise; + JSValue reason; +} JSRejectedPromiseEntry; + typedef struct JSThreadState { struct list_head os_rw_handlers; /* list of JSOSRWHandler.link */ struct list_head os_signal_handlers; /* list JSOSSignalHandler.link */ struct list_head os_timers; /* list of JSOSTimer.link */ struct list_head port_list; /* list of JSWorkerMessageHandler.link */ + struct list_head rejected_promise_list; /* list of JSRejectedPromiseEntry.link */ int eval_script_recurse; /* only used in the main thread */ int next_timer_id; /* for setTimeout() */ /* not used in the main thread */ @@ -160,6 +167,7 @@ static BOOL my_isdigit(int c) return (c >= '0' && c <= '9'); } +/* XXX: use 'o' and 'O' for object using JS_PrintValue() ? */ static JSValue js_printf_internal(JSContext *ctx, int argc, JSValueConst *argv, FILE *fp) { @@ -583,17 +591,101 @@ int js_module_set_import_meta(JSContext *ctx, JSValueConst func_val, return 0; } -JSModuleDef *js_module_loader(JSContext *ctx, - const char *module_name, void *opaque) +static int json_module_init(JSContext *ctx, JSModuleDef *m) +{ + JSValue val; + val = JS_GetModulePrivateValue(ctx, m); + JS_SetModuleExport(ctx, m, "default", val); + return 0; +} + +static JSModuleDef *create_json_module(JSContext *ctx, const char *module_name, JSValue val) { JSModuleDef *m; + m = JS_NewCModule(ctx, module_name, json_module_init); + if (!m) { + JS_FreeValue(ctx, val); + return NULL; + } + /* only export the "default" symbol which will contain the JSON object */ + JS_AddModuleExport(ctx, m, "default"); + JS_SetModulePrivateValue(ctx, m, val); + return m; +} + +/* in order to conform with the specification, only the keys should be + tested and not the associated values. */ +int js_module_check_attributes(JSContext *ctx, void *opaque, + JSValueConst attributes) +{ + JSPropertyEnum *tab; + uint32_t i, len; + int ret; + const char *cstr; + size_t cstr_len; + + if (JS_GetOwnPropertyNames(ctx, &tab, &len, attributes, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK)) + return -1; + ret = 0; + for(i = 0; i < len; i++) { + cstr = JS_AtomToCStringLen(ctx, &cstr_len, tab[i].atom); + if (!cstr) { + ret = -1; + break; + } + if (!(cstr_len == 4 && !memcmp(cstr, "type", cstr_len))) { + JS_ThrowTypeError(ctx, "import attribute '%s' is not supported", cstr); + ret = -1; + } + JS_FreeCString(ctx, cstr); + if (ret) + break; + } + JS_FreePropertyEnum(ctx, tab, len); + return ret; +} +/* return > 0 if the attributes indicate a JSON module */ +int js_module_test_json(JSContext *ctx, JSValueConst attributes) +{ + JSValue str; + const char *cstr; + size_t len; + BOOL res; + + if (JS_IsUndefined(attributes)) + return FALSE; + str = JS_GetPropertyStr(ctx, attributes, "type"); + if (!JS_IsString(str)) + return FALSE; + cstr = JS_ToCStringLen(ctx, &len, str); + JS_FreeValue(ctx, str); + if (!cstr) + return FALSE; + /* XXX: raise an error if unknown type ? */ + if (len == 4 && !memcmp(cstr, "json", len)) { + res = 1; + } else if (len == 5 && !memcmp(cstr, "json5", len)) { + res = 2; + } else { + res = 0; + } + JS_FreeCString(ctx, cstr); + return res; +} + +JSModuleDef *js_module_loader(JSContext *ctx, + const char *module_name, void *opaque, + JSValueConst attributes) +{ + JSModuleDef *m; + int res; + if (has_suffix(module_name, ".so")) { m = js_module_loader_so(ctx, module_name); } else { size_t buf_len; uint8_t *buf; - JSValue func_val; buf = js_load_file(ctx, &buf_len, module_name); if (!buf) { @@ -601,18 +693,36 @@ JSModuleDef *js_module_loader(JSContext *ctx, module_name); return NULL; } - - /* compile the module */ - func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name, - JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); - js_free(ctx, buf); - if (JS_IsException(func_val)) - return NULL; - /* XXX: could propagate the exception */ - js_module_set_import_meta(ctx, func_val, TRUE, FALSE); - /* the module is already referenced, so we must free it */ - m = JS_VALUE_GET_PTR(func_val); - JS_FreeValue(ctx, func_val); + res = js_module_test_json(ctx, attributes); + if (has_suffix(module_name, ".json") || res > 0) { + /* compile as JSON or JSON5 depending on "type" */ + JSValue val; + int flags; + if (res == 2) + flags = JS_PARSE_JSON_EXT; + else + flags = 0; + val = JS_ParseJSON2(ctx, (char *)buf, buf_len, module_name, flags); + js_free(ctx, buf); + if (JS_IsException(val)) + return NULL; + m = create_json_module(ctx, module_name, val); + if (!m) + return NULL; + } else { + JSValue func_val; + /* compile the module */ + func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name, + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + js_free(ctx, buf); + if (JS_IsException(func_val)) + return NULL; + /* XXX: could propagate the exception */ + js_module_set_import_meta(ctx, func_val, TRUE, FALSE); + /* the module is already referenced, so we must free it */ + m = JS_VALUE_GET_PTR(func_val); + JS_FreeValue(ctx, func_val); + } } return m; } @@ -1083,10 +1193,16 @@ static JSValue js_std_file_printf(JSContext *ctx, JSValueConst this_val, return js_printf_internal(ctx, argc, argv, f); } +static void js_print_value_write(void *opaque, const char *buf, size_t len) +{ + FILE *fo = opaque; + fwrite(buf, 1, len, fo); +} + static JSValue js_std_file_printObject(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { - JS_PrintValue(ctx, stdout, argv[0], NULL); + JS_PrintValue(ctx, js_print_value_write, stdout, argv[0], NULL); return JS_UNDEFINED; } @@ -2924,9 +3040,7 @@ static char **build_envp(JSContext *ctx, JSValueConst obj) JS_FreeCString(ctx, str); } done: - for(i = 0; i < len; i++) - JS_FreeAtom(ctx, tab[i].atom); - js_free(ctx, tab); + JS_FreePropertyEnum(ctx, tab, len); return envp; fail: if (envp) { @@ -3466,7 +3580,7 @@ static void *worker_func(void *opaque) JS_SetStripInfo(rt, args->strip_flags); js_std_init_handlers(rt); - JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL); + JS_SetModuleLoaderFunc2(rt, NULL, js_module_loader, js_module_check_attributes, NULL); /* set the pipe to communicate with the parent */ ts = JS_GetRuntimeOpaque(rt); @@ -3914,7 +4028,7 @@ static JSValue js_print(JSContext *ctx, JSValueConst this_val, fwrite(str, 1, len, stdout); JS_FreeCString(ctx, str); } else { - JS_PrintValue(ctx, stdout, v, NULL); + JS_PrintValue(ctx, js_print_value_write, stdout, v, NULL); } } putchar('\n'); @@ -3979,6 +4093,7 @@ void js_std_init_handlers(JSRuntime *rt) init_list_head(&ts->os_signal_handlers); init_list_head(&ts->os_timers); init_list_head(&ts->port_list); + init_list_head(&ts->rejected_promise_list); ts->next_timer_id = 1; JS_SetRuntimeOpaque(rt, ts); @@ -4016,6 +4131,13 @@ void js_std_free_handlers(JSRuntime *rt) free_timer(rt, th); } + list_for_each_safe(el, el1, &ts->rejected_promise_list) { + JSRejectedPromiseEntry *rp = list_entry(el, JSRejectedPromiseEntry, link); + JS_FreeValueRT(rt, rp->promise); + JS_FreeValueRT(rt, rp->reason); + free(rp); + } + #ifdef USE_WORKER /* XXX: free port_list ? */ js_free_message_pipe(ts->recv_pipe); @@ -4028,7 +4150,7 @@ void js_std_free_handlers(JSRuntime *rt) static void js_std_dump_error1(JSContext *ctx, JSValueConst exception_val) { - JS_PrintValue(ctx, stderr, exception_val, NULL); + JS_PrintValue(ctx, js_print_value_write, stderr, exception_val, NULL); fputc('\n', stderr); } @@ -4041,13 +4163,66 @@ void js_std_dump_error(JSContext *ctx) JS_FreeValue(ctx, exception_val); } +static JSRejectedPromiseEntry *find_rejected_promise(JSContext *ctx, JSThreadState *ts, + JSValueConst promise) +{ + struct list_head *el; + + list_for_each(el, &ts->rejected_promise_list) { + JSRejectedPromiseEntry *rp = list_entry(el, JSRejectedPromiseEntry, link); + if (JS_SameValue(ctx, rp->promise, promise)) + return rp; + } + return NULL; +} + void js_std_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, JSValueConst reason, BOOL is_handled, void *opaque) { + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = JS_GetRuntimeOpaque(rt); + JSRejectedPromiseEntry *rp; + if (!is_handled) { - fprintf(stderr, "Possibly unhandled promise rejection: "); - js_std_dump_error1(ctx, reason); + /* add a new entry if needed */ + rp = find_rejected_promise(ctx, ts, promise); + if (!rp) { + rp = malloc(sizeof(*rp)); + if (rp) { + rp->promise = JS_DupValue(ctx, promise); + rp->reason = JS_DupValue(ctx, reason); + list_add_tail(&rp->link, &ts->rejected_promise_list); + } + } + } else { + /* the rejection is handled, so the entry can be removed if present */ + rp = find_rejected_promise(ctx, ts, promise); + if (rp) { + JS_FreeValue(ctx, rp->promise); + JS_FreeValue(ctx, rp->reason); + list_del(&rp->link); + free(rp); + } + } +} + +/* check if there are pending promise rejections. It must be done + asynchrously in case a rejected promise is handled later. Currently + we do it once the application is about to sleep. It could be done + more often if needed. */ +static void js_std_promise_rejection_check(JSContext *ctx) +{ + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = JS_GetRuntimeOpaque(rt); + struct list_head *el; + + if (unlikely(!list_empty(&ts->rejected_promise_list))) { + list_for_each(el, &ts->rejected_promise_list) { + JSRejectedPromiseEntry *rp = list_entry(el, JSRejectedPromiseEntry, link); + fprintf(stderr, "Possibly unhandled promise rejection: "); + js_std_dump_error1(ctx, rp->reason); + } exit(1); } } @@ -4055,21 +4230,21 @@ void js_std_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, /* main loop which calls the user JS callbacks */ void js_std_loop(JSContext *ctx) { - JSContext *ctx1; int err; for(;;) { /* execute the pending jobs */ for(;;) { - err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); + err = JS_ExecutePendingJob(JS_GetRuntime(ctx), NULL); if (err <= 0) { - if (err < 0) { - js_std_dump_error(ctx1); - } + if (err < 0) + js_std_dump_error(ctx); break; } } + js_std_promise_rejection_check(ctx); + if (!os_poll_func || os_poll_func(ctx)) break; } @@ -4094,13 +4269,14 @@ JSValue js_std_await(JSContext *ctx, JSValue obj) JS_FreeValue(ctx, obj); break; } else if (state == JS_PROMISE_PENDING) { - JSContext *ctx1; int err; - err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); + err = JS_ExecutePendingJob(JS_GetRuntime(ctx), NULL); if (err < 0) { - js_std_dump_error(ctx1); + js_std_dump_error(ctx); } if (err == 0) { + js_std_promise_rejection_check(ctx); + if (os_poll_func) os_poll_func(ctx); } @@ -4124,6 +4300,7 @@ void js_std_eval_binary(JSContext *ctx, const uint8_t *buf, size_t buf_len, if (JS_VALUE_GET_TAG(obj) == JS_TAG_MODULE) { js_module_set_import_meta(ctx, obj, FALSE, FALSE); } + JS_FreeValue(ctx, obj); } else { if (JS_VALUE_GET_TAG(obj) == JS_TAG_MODULE) { if (JS_ResolveModule(ctx, obj) < 0) { @@ -4144,3 +4321,22 @@ void js_std_eval_binary(JSContext *ctx, const uint8_t *buf, size_t buf_len, JS_FreeValue(ctx, val); } } + +void js_std_eval_binary_json_module(JSContext *ctx, + const uint8_t *buf, size_t buf_len, + const char *module_name) +{ + JSValue obj; + JSModuleDef *m; + + obj = JS_ReadObject(ctx, buf, buf_len, 0); + if (JS_IsException(obj)) + goto exception; + m = create_json_module(ctx, module_name, obj); + if (!m) { + exception: + js_std_dump_error(ctx); + exit(1); + } +} + diff --git a/src/couch_quickjs/quickjs/quickjs-libc.h b/src/couch_quickjs/quickjs/quickjs-libc.h index 850484f361..5c8301b717 100644 --- a/src/couch_quickjs/quickjs/quickjs-libc.h +++ b/src/couch_quickjs/quickjs/quickjs-libc.h @@ -44,10 +44,16 @@ void js_std_dump_error(JSContext *ctx); uint8_t *js_load_file(JSContext *ctx, size_t *pbuf_len, const char *filename); int js_module_set_import_meta(JSContext *ctx, JSValueConst func_val, JS_BOOL use_realpath, JS_BOOL is_main); +int js_module_test_json(JSContext *ctx, JSValueConst attributes); +int js_module_check_attributes(JSContext *ctx, void *opaque, JSValueConst attributes); JSModuleDef *js_module_loader(JSContext *ctx, - const char *module_name, void *opaque); + const char *module_name, void *opaque, + JSValueConst attributes); void js_std_eval_binary(JSContext *ctx, const uint8_t *buf, size_t buf_len, int flags); +void js_std_eval_binary_json_module(JSContext *ctx, + const uint8_t *buf, size_t buf_len, + const char *module_name); void js_std_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, JSValueConst reason, JS_BOOL is_handled, void *opaque); diff --git a/src/couch_quickjs/quickjs/quickjs-opcode.h b/src/couch_quickjs/quickjs/quickjs-opcode.h index e721e17072..d93852133d 100644 --- a/src/couch_quickjs/quickjs/quickjs-opcode.h +++ b/src/couch_quickjs/quickjs/quickjs-opcode.h @@ -121,21 +121,16 @@ DEF( apply_eval, 3, 2, 1, u16) /* func array -> ret_eval */ DEF( regexp, 1, 2, 1, none) /* create a RegExp object from the pattern and a bytecode string */ DEF( get_super, 1, 1, 1, none) -DEF( import, 1, 1, 1, none) /* dynamic module import */ +DEF( import, 1, 2, 1, none) /* dynamic module import */ -DEF( check_var, 5, 0, 1, atom) /* check if a variable exists */ -DEF( get_var_undef, 5, 0, 1, atom) /* push undefined if the variable does not exist */ -DEF( get_var, 5, 0, 1, atom) /* throw an exception if the variable does not exist */ -DEF( put_var, 5, 1, 0, atom) /* must come after get_var */ -DEF( put_var_init, 5, 1, 0, atom) /* must come after put_var. Used to initialize a global lexical variable */ -DEF( put_var_strict, 5, 2, 0, atom) /* for strict mode variable write */ +DEF( get_var_undef, 3, 0, 1, var_ref) /* push undefined if the variable does not exist */ +DEF( get_var, 3, 0, 1, var_ref) /* throw an exception if the variable does not exist */ +DEF( put_var, 3, 1, 0, var_ref) /* must come after get_var */ +DEF( put_var_init, 3, 1, 0, var_ref) /* must come after put_var. Used to initialize a global lexical variable */ DEF( get_ref_value, 1, 2, 3, none) DEF( put_ref_value, 1, 3, 0, none) -DEF( define_var, 6, 0, 0, atom_u8) -DEF(check_define_var, 6, 0, 0, atom_u8) -DEF( define_func, 6, 1, 0, atom_u8) DEF( get_field, 5, 1, 1, atom) DEF( get_field2, 5, 1, 2, atom) DEF( put_field, 5, 2, 0, atom) diff --git a/src/couch_quickjs/quickjs/quickjs.c b/src/couch_quickjs/quickjs/quickjs.c index 5ff86eedf4..07ddb6aa1a 100644 --- a/src/couch_quickjs/quickjs/quickjs.c +++ b/src/couch_quickjs/quickjs/quickjs.c @@ -103,6 +103,7 @@ //#define DUMP_ATOMS /* dump atoms in JS_FreeContext */ //#define DUMP_SHAPES /* dump shapes in JS_FreeContext */ //#define DUMP_MODULE_RESOLVE +//#define DUMP_MODULE_EXEC //#define DUMP_PROMISE //#define DUMP_READ_OBJECT //#define DUMP_ROPE_REBALANCE @@ -147,6 +148,7 @@ enum { JS_CLASS_UINT32_ARRAY, /* u.array (typed_array) */ JS_CLASS_BIG_INT64_ARRAY, /* u.array (typed_array) */ JS_CLASS_BIG_UINT64_ARRAY, /* u.array (typed_array) */ + JS_CLASS_FLOAT16_ARRAY, /* u.array (typed_array) */ JS_CLASS_FLOAT32_ARRAY, /* u.array (typed_array) */ JS_CLASS_FLOAT64_ARRAY, /* u.array (typed_array) */ JS_CLASS_DATAVIEW, /* u.typed_array */ @@ -155,12 +157,16 @@ enum { JS_CLASS_SET, /* u.map_state */ JS_CLASS_WEAKMAP, /* u.map_state */ JS_CLASS_WEAKSET, /* u.map_state */ + JS_CLASS_ITERATOR, /* u.map_iterator_data */ + JS_CLASS_ITERATOR_HELPER, /* u.iterator_helper_data */ + JS_CLASS_ITERATOR_WRAP, /* u.iterator_wrap_data */ JS_CLASS_MAP_ITERATOR, /* u.map_iterator_data */ JS_CLASS_SET_ITERATOR, /* u.map_iterator_data */ JS_CLASS_ARRAY_ITERATOR, /* u.array_iterator_data */ JS_CLASS_STRING_ITERATOR, /* u.array_iterator_data */ JS_CLASS_REGEXP_STRING_ITERATOR, /* u.regexp_string_iterator_data */ JS_CLASS_GENERATOR, /* u.generator_data */ + JS_CLASS_GLOBAL_OBJECT, /* u.global_object */ JS_CLASS_PROXY, /* u.proxy_data */ JS_CLASS_PROMISE, /* u.promise_data */ JS_CLASS_PROMISE_RESOLVE_FUNCTION, /* u.promise_function_data */ @@ -279,7 +285,12 @@ struct JSRuntime { struct list_head job_list; /* list of JSJobEntry.link */ JSModuleNormalizeFunc *module_normalize_func; - JSModuleLoaderFunc *module_loader_func; + BOOL module_loader_has_attr; + union { + JSModuleLoaderFunc *module_loader_func; + JSModuleLoaderFunc2 *module_loader_func2; + } u; + JSModuleCheckSupportedImportAttributes *module_check_attrs; void *module_loader_opaque; /* timestamp for internal use in module evaluation */ int64_t module_async_evaluation_next_timestamp; @@ -334,6 +345,7 @@ typedef enum { JS_GC_OBJ_TYPE_VAR_REF, JS_GC_OBJ_TYPE_ASYNC_FUNCTION, JS_GC_OBJ_TYPE_JS_CONTEXT, + JS_GC_OBJ_TYPE_MODULE, } JSGCObjectTypeEnum; /* header for GC objects. GC objects are C data structures with a @@ -342,7 +354,8 @@ typedef enum { struct JSGCObjectHeader { int ref_count; /* must come first, 32-bit */ JSGCObjectTypeEnum gc_obj_type : 4; - uint8_t mark : 4; /* used by the GC */ + uint8_t mark : 1; /* used by the GC */ + uint8_t dummy0: 3; uint8_t dummy1; /* not used by the GC */ uint16_t dummy2; /* not used by the GC */ struct list_head link; @@ -366,6 +379,8 @@ typedef struct JSVarRef { int __gc_ref_count; /* corresponds to header.ref_count */ uint8_t __gc_mark; /* corresponds to header.mark/gc_obj_type */ uint8_t is_detached; + uint8_t is_lexical; /* only used with global variables */ + uint8_t is_const; /* only used with global variables */ }; }; JSValue *pvalue; /* pointer to the value, either on the stack or @@ -435,7 +450,14 @@ struct JSContext { uint16_t binary_object_count; int binary_object_size; - + /* TRUE if the array prototype is "normal": + - no small index properties which are get/set or non writable + - its prototype is Object.prototype + - Object.prototype has no small index properties which are get/set or non writable + - the prototype of Object.prototype is null (always true as it is immutable) + */ + uint8_t std_array_prototype; + JSShape *array_shape; /* initial shape for Array objects */ JSValue *class_proto; @@ -445,7 +467,7 @@ struct JSContext { JSValue regexp_ctor; JSValue promise_ctor; JSValue native_error_proto[JS_NATIVE_ERROR_COUNT]; - JSValue iterator_proto; + JSValue iterator_ctor; JSValue async_iterator_proto; JSValue array_proto_values; JSValue throw_type_error; @@ -523,11 +545,23 @@ typedef struct JSStringRope { JSValue right; /* might be the empty string */ } JSStringRope; +typedef enum { + JS_CLOSURE_LOCAL, /* 'var_idx' is the index of a local variable in the parent function */ + JS_CLOSURE_ARG, /* 'var_idx' is the index of a argument variable in the parent function */ + JS_CLOSURE_REF, /* 'var_idx' is the index of a closure variable in the parent function */ + JS_CLOSURE_GLOBAL_REF, /* 'var_idx' in the index of a closure + variable in the parent function + referencing a global variable */ + JS_CLOSURE_GLOBAL_DECL, /* global variable declaration (eval code only) */ + JS_CLOSURE_GLOBAL, /* global variable (eval code only) */ + JS_CLOSURE_MODULE_DECL, /* definition of a module variable (eval code only) */ + JS_CLOSURE_MODULE_IMPORT, /* definition of a module import (eval code only) */ +} JSClosureTypeEnum; + typedef struct JSClosureVar { - uint8_t is_local : 1; - uint8_t is_arg : 1; - uint8_t is_const : 1; - uint8_t is_lexical : 1; + JSClosureTypeEnum closure_type : 3; + uint8_t is_lexical : 1; /* lexical variable */ + uint8_t is_const : 1; /* const variable (is_lexical = 1 if is_const = 1 */ uint8_t var_kind : 4; /* see JSVarKindEnum */ /* 8 bits available */ uint16_t var_idx; /* is_local = TRUE: index to a normal variable of the @@ -557,6 +591,7 @@ typedef enum { JS_VAR_PRIVATE_GETTER, JS_VAR_PRIVATE_SETTER, /* must come after JS_VAR_PRIVATE_GETTER */ JS_VAR_PRIVATE_GETTER_SETTER, /* must come after JS_VAR_PRIVATE_SETTER */ + JS_VAR_GLOBAL_FUNCTION_DECL, /* global function definition, only in JSVarDef */ } JSVarKindEnum; /* XXX: could use a different structure in bytecode functions to save @@ -678,6 +713,7 @@ typedef struct JSProxyData { typedef struct JSArrayBuffer { int byte_length; /* 0 if detached */ + int max_byte_length; /* -1 if not resizable; >= byte_length otherwise */ uint8_t detached; uint8_t shared; /* if shared, the array buffer cannot be detached */ uint8_t *data; /* NULL if detached */ @@ -690,10 +726,15 @@ typedef struct JSTypedArray { struct list_head link; /* link to arraybuffer */ JSObject *obj; /* back pointer to the TypedArray/DataView object */ JSObject *buffer; /* based array buffer */ - uint32_t offset; /* offset in the array buffer */ - uint32_t length; /* length in the array buffer */ + uint32_t offset; /* byte offset in the array buffer */ + uint32_t length; /* byte length in the array buffer */ + BOOL track_rab; /* auto-track length of backing array buffer */ } JSTypedArray; +typedef struct JSGlobalObject { + JSValue uninitialized_vars; /* hidden object containing the list of uninitialized variables */ +} JSGlobalObject; + typedef struct JSAsyncFunctionState { JSGCObjectHeader header; JSValue this_val; /* 'this' argument */ @@ -755,6 +796,7 @@ typedef struct { typedef struct JSReqModuleEntry { JSAtom module_name; JSModuleDef *module; /* used using resolution */ + JSValue attributes; /* JS_UNDEFINED or an object contains the attributes as key/value */ } JSReqModuleEntry; typedef enum JSExportTypeEnum { @@ -797,7 +839,7 @@ typedef enum { } JSModuleStatus; struct JSModuleDef { - JSRefCountHeader header; /* must come first, 32-bit */ + JSGCObjectHeader header; /* must come first */ JSAtom module_name; struct list_head link; @@ -832,7 +874,8 @@ struct JSModuleDef { int async_parent_modules_count; int async_parent_modules_size; int pending_async_dependencies; - BOOL async_evaluation; + BOOL async_evaluation; /* true: async_evaluation_timestamp corresponds to [[AsyncEvaluationOrder]] + false: [[AsyncEvaluationOrder]] is UNSET or DONE */ int64_t async_evaluation_timestamp; JSModuleDef *cycle_root; JSValue promise; /* corresponds to spec field: capability */ @@ -843,11 +886,12 @@ struct JSModuleDef { BOOL eval_has_exception : 8; JSValue eval_exception; JSValue meta_obj; /* for import.meta */ + JSValue private_value; /* private value for C modules */ }; typedef struct JSJobEntry { struct list_head link; - JSContext *ctx; + JSContext *realm; JSJobFunc *job_func; int argc; JSValue argv[0]; @@ -873,7 +917,6 @@ typedef struct JSProperty { #define JS_PROP_INITIAL_SIZE 2 #define JS_PROP_INITIAL_HASH_SIZE 4 /* must be a power of two */ -#define JS_ARRAY_INITIAL_SIZE 2 typedef struct JSShapeProperty { uint32_t hash_next : 26; /* 0 if last in list */ @@ -888,10 +931,6 @@ struct JSShape { /* true if the shape is inserted in the shape hash table. If not, JSShape.hash is not valid */ uint8_t is_hashed; - /* If true, the shape may have small array index properties 'n' with 0 - <= n <= 2^31-1. If false, the shape is guaranteed not to have - small array index properties */ - uint8_t has_small_array_index; uint32_t hash; /* current hash value */ uint32_t prop_hash_mask; int prop_size; /* allocated properties */ @@ -907,7 +946,8 @@ struct JSObject { JSGCObjectHeader header; struct { int __gc_ref_count; /* corresponds to header.ref_count */ - uint8_t __gc_mark; /* corresponds to header.mark/gc_obj_type */ + uint8_t __gc_mark : 7; /* corresponds to header.mark/gc_obj_type */ + uint8_t is_prototype : 1; /* object may be used as prototype */ uint8_t extensible : 1; uint8_t free_mark : 1; /* only used when freeing objects with cycles */ @@ -938,6 +978,8 @@ struct JSObject { struct JSArrayIteratorData *array_iterator_data; /* JS_CLASS_ARRAY_ITERATOR, JS_CLASS_STRING_ITERATOR */ struct JSRegExpStringIteratorData *regexp_string_iterator_data; /* JS_CLASS_REGEXP_STRING_ITERATOR */ struct JSGeneratorData *generator_data; /* JS_CLASS_GENERATOR */ + struct JSIteratorHelperData *iterator_helper_data; /* JS_CLASS_ITERATOR_HELPER */ + struct JSIteratorWrapData *iterator_wrap_data; /* JS_CLASS_ITERATOR_WRAP */ struct JSProxyData *proxy_data; /* JS_CLASS_PROXY */ struct JSPromiseData *promise_data; /* JS_CLASS_PROMISE */ struct JSPromiseFunctionData *promise_function_data; /* JS_CLASS_PROMISE_RESOLVE_FUNCTION, JS_CLASS_PROMISE_REJECT_FUNCTION */ @@ -974,6 +1016,7 @@ struct JSObject { uint32_t *uint32_ptr; /* JS_CLASS_UINT32_ARRAY */ int64_t *int64_ptr; /* JS_CLASS_INT64_ARRAY */ uint64_t *uint64_ptr; /* JS_CLASS_UINT64_ARRAY */ + uint16_t *fp16_ptr; /* JS_CLASS_FLOAT16_ARRAY */ float *float_ptr; /* JS_CLASS_FLOAT32_ARRAY */ double *double_ptr; /* JS_CLASS_FLOAT64_ARRAY */ } u; @@ -981,6 +1024,7 @@ struct JSObject { } array; /* 12/20 bytes */ JSRegExp regexp; /* JS_CLASS_REGEXP: 8/16 bytes */ JSValue object_data; /* for JS_SetObjectData(): 8/16/16 bytes */ + JSGlobalObject global_object; } u; }; @@ -1078,15 +1122,16 @@ static __exception int JS_ToArrayLengthFree(JSContext *ctx, uint32_t *plen, static JSValue JS_EvalObject(JSContext *ctx, JSValueConst this_obj, JSValueConst val, int flags, int scope_idx); JSValue __attribute__((format(printf, 2, 3))) JS_ThrowInternalError(JSContext *ctx, const char *fmt, ...); -static void js_print_atom(JSRuntime *rt, FILE *fo, JSAtom atom); static __maybe_unused void JS_DumpAtoms(JSRuntime *rt); static __maybe_unused void JS_DumpString(JSRuntime *rt, const JSString *p); static __maybe_unused void JS_DumpObjectHeader(JSRuntime *rt); static __maybe_unused void JS_DumpObject(JSRuntime *rt, JSObject *p); static __maybe_unused void JS_DumpGCObject(JSRuntime *rt, JSGCObjectHeader *p); +static __maybe_unused void JS_DumpAtom(JSContext *ctx, const char *str, JSAtom atom); static __maybe_unused void JS_DumpValueRT(JSRuntime *rt, const char *str, JSValueConst val); static __maybe_unused void JS_DumpValue(JSContext *ctx, const char *str, JSValueConst val); static __maybe_unused void JS_DumpShapes(JSRuntime *rt); +static void js_dump_value_write(void *opaque, const char *buf, size_t len); static JSValue js_function_apply(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); static void js_array_finalizer(JSRuntime *rt, JSValue val); @@ -1121,12 +1166,21 @@ static void js_map_iterator_mark(JSRuntime *rt, JSValueConst val, static void js_array_iterator_finalizer(JSRuntime *rt, JSValue val); static void js_array_iterator_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); +static void js_iterator_helper_finalizer(JSRuntime *rt, JSValue val); +static void js_iterator_helper_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func); +static void js_iterator_wrap_finalizer(JSRuntime *rt, JSValue val); +static void js_iterator_wrap_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func); static void js_regexp_string_iterator_finalizer(JSRuntime *rt, JSValue val); static void js_regexp_string_iterator_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); static void js_generator_finalizer(JSRuntime *rt, JSValue obj); static void js_generator_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); +static void js_global_object_finalizer(JSRuntime *rt, JSValue obj); +static void js_global_object_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func); static void js_promise_finalizer(JSRuntime *rt, JSValue val); static void js_promise_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); @@ -1147,8 +1201,8 @@ static int JS_ToUint8ClampFree(JSContext *ctx, int32_t *pres, JSValue val); static JSValue js_new_string8_len(JSContext *ctx, const char *buf, int len); static JSValue js_compile_regexp(JSContext *ctx, JSValueConst pattern, JSValueConst flags); -static JSValue js_regexp_constructor_internal(JSContext *ctx, JSValueConst ctor, - JSValue pattern, JSValue bc); +static JSValue js_regexp_set_internal(JSContext *ctx, JSValue obj, + JSValue pattern, JSValue bc); static void gc_decref(JSRuntime *rt); static int JS_NewClass1(JSRuntime *rt, JSClassID class_id, const JSClassDef *class_def, JSAtom name); @@ -1181,11 +1235,14 @@ static int js_string_memcmp(const JSString *p1, int pos1, const JSString *p2, int pos2, int len); static JSValue js_array_buffer_constructor3(JSContext *ctx, JSValueConst new_target, - uint64_t len, JSClassID class_id, + uint64_t len, uint64_t *max_len, + JSClassID class_id, uint8_t *buf, JSFreeArrayBufferDataFunc *free_func, void *opaque, BOOL alloc_flag); +static void js_array_buffer_free(JSRuntime *rt, void *opaque, void *ptr); static JSArrayBuffer *js_get_array_buffer(JSContext *ctx, JSValueConst obj); +static BOOL array_buffer_is_resizable(const JSArrayBuffer *abuf); static JSValue js_typed_array_constructor(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, @@ -1193,10 +1250,12 @@ static JSValue js_typed_array_constructor(JSContext *ctx, static JSValue js_typed_array_constructor_ta(JSContext *ctx, JSValueConst new_target, JSValueConst src_obj, - int classid); -static BOOL typed_array_is_detached(JSContext *ctx, JSObject *p); -static uint32_t typed_array_get_length(JSContext *ctx, JSObject *p); + int classid, uint32_t len); +static BOOL typed_array_is_oob(JSObject *p); +static int js_typed_array_get_length_unsafe(JSContext *ctx, JSValueConst obj); static JSValue JS_ThrowTypeErrorDetachedArrayBuffer(JSContext *ctx); +static JSValue JS_ThrowTypeErrorArrayBufferOOB(JSContext *ctx); +static JSVarRef *js_create_var_ref(JSContext *ctx, BOOL is_lexical); static JSVarRef *get_var_ref(JSContext *ctx, JSStackFrame *sf, int var_idx, BOOL is_arg); static void __async_func_free(JSRuntime *rt, JSAsyncFunctionState *s); @@ -1211,11 +1270,11 @@ static void js_async_function_resolve_mark(JSRuntime *rt, JSValueConst val, static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, const char *input, size_t input_len, const char *filename, int flags, int scope_idx); -static void js_free_module_def(JSContext *ctx, JSModuleDef *m); +static void js_free_module_def(JSRuntime *rt, JSModuleDef *m); static void js_mark_module_def(JSRuntime *rt, JSModuleDef *m, JS_MarkFunc *mark_func); static JSValue js_import_meta(JSContext *ctx); -static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier); +static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier, JSValueConst options); static void free_var_ref(JSRuntime *rt, JSVarRef *var_ref); static JSValue js_new_promise_capability(JSContext *ctx, JSValue *resolving_funcs, @@ -1228,6 +1287,8 @@ static JSValue js_promise_resolve(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); static JSValue js_promise_then(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); +static BOOL js_string_eq(JSContext *ctx, + const JSString *p1, const JSString *p2); static int js_string_compare(JSContext *ctx, const JSString *p1, const JSString *p2); static JSValue JS_ToNumber(JSContext *ctx, JSValueConst val); @@ -1239,7 +1300,7 @@ static JSValue JS_ToNumberFree(JSContext *ctx, JSValue val); static int JS_GetOwnPropertyInternal(JSContext *ctx, JSPropertyDescriptor *desc, JSObject *p, JSAtom prop); static void js_free_desc(JSContext *ctx, JSPropertyDescriptor *desc); -static void JS_AddIntrinsicBasicObjects(JSContext *ctx); +static int JS_AddIntrinsicBasicObjects(JSContext *ctx); static void js_free_shape(JSRuntime *rt, JSShape *sh); static void js_free_shape_null(JSRuntime *rt, JSShape *sh); static int js_shape_prepare_update(JSContext *ctx, JSObject *p, @@ -1286,6 +1347,8 @@ static JSValue get_date_string(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); static JSValue js_error_toString(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); +static JSVarRef *js_global_object_find_uninitialized_var(JSContext *ctx, JSObject *p, + JSAtom atom, BOOL is_lexical); static const JSClassExoticMethods js_arguments_exotic_methods; static const JSClassExoticMethods js_string_exotic_methods; @@ -1458,6 +1521,23 @@ static inline void js_dbuf_init(JSContext *ctx, DynBuf *s) dbuf_init2(s, ctx->rt, (DynBufReallocFunc *)js_realloc_rt); } +static void *js_realloc_bytecode_rt(void *opaque, void *ptr, size_t size) +{ + JSRuntime *rt = opaque; + if (size > (INT32_MAX / 2)) { + /* the bytecode cannot be larger than 2G. Leave some slack to + avoid some overflows. */ + return NULL; + } else { + return rt->mf.js_realloc(&rt->malloc_state, ptr, size); + } +} + +static inline void js_dbuf_bytecode_init(JSContext *ctx, DynBuf *s) +{ + dbuf_init2(s, ctx->rt, js_realloc_bytecode_rt); +} + static inline int is_digit(int c) { return c >= '0' && c <= '9'; } @@ -1502,6 +1582,7 @@ static JSClassShortDef const js_std_class_def[] = { { JS_ATOM_Uint32Array, js_typed_array_finalizer, js_typed_array_mark }, /* JS_CLASS_UINT32_ARRAY */ { JS_ATOM_BigInt64Array, js_typed_array_finalizer, js_typed_array_mark }, /* JS_CLASS_BIG_INT64_ARRAY */ { JS_ATOM_BigUint64Array, js_typed_array_finalizer, js_typed_array_mark }, /* JS_CLASS_BIG_UINT64_ARRAY */ + { JS_ATOM_Float16Array, js_typed_array_finalizer, js_typed_array_mark }, /* JS_CLASS_FLOAT16_ARRAY */ { JS_ATOM_Float32Array, js_typed_array_finalizer, js_typed_array_mark }, /* JS_CLASS_FLOAT32_ARRAY */ { JS_ATOM_Float64Array, js_typed_array_finalizer, js_typed_array_mark }, /* JS_CLASS_FLOAT64_ARRAY */ { JS_ATOM_DataView, js_typed_array_finalizer, js_typed_array_mark }, /* JS_CLASS_DATAVIEW */ @@ -1510,12 +1591,16 @@ static JSClassShortDef const js_std_class_def[] = { { JS_ATOM_Set, js_map_finalizer, js_map_mark }, /* JS_CLASS_SET */ { JS_ATOM_WeakMap, js_map_finalizer, js_map_mark }, /* JS_CLASS_WEAKMAP */ { JS_ATOM_WeakSet, js_map_finalizer, js_map_mark }, /* JS_CLASS_WEAKSET */ + { JS_ATOM_Iterator, NULL, NULL }, /* JS_CLASS_ITERATOR */ + { JS_ATOM_IteratorHelper, js_iterator_helper_finalizer, js_iterator_helper_mark }, /* JS_CLASS_ITERATOR_HELPER */ + { JS_ATOM_IteratorWrap, js_iterator_wrap_finalizer, js_iterator_wrap_mark }, /* JS_CLASS_ITERATOR_WRAP */ { JS_ATOM_Map_Iterator, js_map_iterator_finalizer, js_map_iterator_mark }, /* JS_CLASS_MAP_ITERATOR */ { JS_ATOM_Set_Iterator, js_map_iterator_finalizer, js_map_iterator_mark }, /* JS_CLASS_SET_ITERATOR */ { JS_ATOM_Array_Iterator, js_array_iterator_finalizer, js_array_iterator_mark }, /* JS_CLASS_ARRAY_ITERATOR */ { JS_ATOM_String_Iterator, js_array_iterator_finalizer, js_array_iterator_mark }, /* JS_CLASS_STRING_ITERATOR */ { JS_ATOM_RegExp_String_Iterator, js_regexp_string_iterator_finalizer, js_regexp_string_iterator_mark }, /* JS_CLASS_REGEXP_STRING_ITERATOR */ { JS_ATOM_Generator, js_generator_finalizer, js_generator_mark }, /* JS_CLASS_GENERATOR */ + { JS_ATOM_Object, js_global_object_finalizer, js_global_object_mark }, /* JS_CLASS_GLOBAL_OBJECT */ }; static int init_class_range(JSRuntime *rt, JSClassShortDef const *tab, @@ -1770,7 +1855,7 @@ int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, e = js_malloc(ctx, sizeof(*e) + argc * sizeof(JSValue)); if (!e) return -1; - e->ctx = ctx; + e->realm = JS_DupContext(ctx); e->job_func = job_func; e->argc = argc; for(i = 0; i < argc; i++) { @@ -1786,7 +1871,10 @@ BOOL JS_IsJobPending(JSRuntime *rt) } /* return < 0 if exception, 0 if no job pending, 1 if a job was - executed successfully. the context of the job is stored in '*pctx' */ + executed successfully. The context of the job is stored in '*pctx' + if pctx != NULL. It may be NULL if the context was already + destroyed or if no job was pending. The 'pctx' parameter is now + absolete. */ int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx) { JSContext *ctx; @@ -1795,15 +1883,16 @@ int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx) int i, ret; if (list_empty(&rt->job_list)) { - *pctx = NULL; + if (pctx) + *pctx = NULL; return 0; } /* get the first pending job and execute it */ e = list_entry(rt->job_list.next, JSJobEntry, link); list_del(&e->link); - ctx = e->ctx; - res = e->job_func(e->ctx, e->argc, (JSValueConst *)e->argv); + ctx = e->realm; + res = e->job_func(ctx, e->argc, (JSValueConst *)e->argv); for(i = 0; i < e->argc; i++) JS_FreeValue(ctx, e->argv[i]); if (JS_IsException(res)) @@ -1812,7 +1901,13 @@ int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx) ret = 1; JS_FreeValue(ctx, res); js_free(ctx, e); - *pctx = ctx; + if (pctx) { + if (ctx->header.ref_count > 1) + *pctx = ctx; + else + *pctx = NULL; + } + JS_FreeContext(ctx); return ret; } @@ -1893,6 +1988,7 @@ void JS_FreeRuntime(JSRuntime *rt) JSJobEntry *e = list_entry(el, JSJobEntry, link); for(i = 0; i < e->argc; i++) JS_FreeValueRT(rt, e->argv[i]); + JS_FreeContext(e->realm); js_free_rt(rt, e); } init_list_head(&rt->job_list); @@ -2091,11 +2187,15 @@ JSContext *JS_NewContextRaw(JSRuntime *rt) for(i = 0; i < rt->class_count; i++) ctx->class_proto[i] = JS_NULL; ctx->array_ctor = JS_NULL; + ctx->iterator_ctor = JS_NULL; ctx->regexp_ctor = JS_NULL; ctx->promise_ctor = JS_NULL; init_list_head(&ctx->loaded_modules); - JS_AddIntrinsicBasicObjects(ctx); + if (JS_AddIntrinsicBasicObjects(ctx)) { + JS_FreeContext(ctx); + return NULL; + } return ctx; } @@ -2107,17 +2207,20 @@ JSContext *JS_NewContext(JSRuntime *rt) if (!ctx) return NULL; - JS_AddIntrinsicBaseObjects(ctx); - JS_AddIntrinsicDate(ctx); - JS_AddIntrinsicEval(ctx); - JS_AddIntrinsicStringNormalize(ctx); - JS_AddIntrinsicRegExp(ctx); - JS_AddIntrinsicJSON(ctx); - JS_AddIntrinsicProxy(ctx); - JS_AddIntrinsicMapSet(ctx); - JS_AddIntrinsicTypedArrays(ctx); - JS_AddIntrinsicPromise(ctx); - JS_AddIntrinsicWeakRef(ctx); + if (JS_AddIntrinsicBaseObjects(ctx) || + JS_AddIntrinsicDate(ctx) || + JS_AddIntrinsicEval(ctx) || + JS_AddIntrinsicStringNormalize(ctx) || + JS_AddIntrinsicRegExp(ctx) || + JS_AddIntrinsicJSON(ctx) || + JS_AddIntrinsicProxy(ctx) || + JS_AddIntrinsicMapSet(ctx) || + JS_AddIntrinsicTypedArrays(ctx) || + JS_AddIntrinsicPromise(ctx) || + JS_AddIntrinsicWeakRef(ctx)) { + JS_FreeContext(ctx); + return NULL; + } return ctx; } @@ -2168,7 +2271,13 @@ static void js_free_modules(JSContext *ctx, JSFreeModuleEnum flag) JSModuleDef *m = list_entry(el, JSModuleDef, link); if (flag == JS_FREE_MODULE_ALL || (flag == JS_FREE_MODULE_NOT_RESOLVED && !m->resolved)) { - js_free_module_def(ctx, m); + /* warning: the module may be referenced elsewhere. It + could be simpler to use an array instead of a list for + 'ctx->loaded_modules' */ + list_del(&m->link); + m->link.prev = NULL; + m->link.next = NULL; + JS_FreeValue(ctx, JS_MKPTR(JS_TAG_MODULE, m)); } } } @@ -2186,11 +2295,9 @@ static void JS_MarkContext(JSRuntime *rt, JSContext *ctx, int i; struct list_head *el; - /* modules are not seen by the GC, so we directly mark the objects - referenced by each module */ list_for_each(el, &ctx->loaded_modules) { JSModuleDef *m = list_entry(el, JSModuleDef, link); - js_mark_module_def(rt, m, mark_func); + JS_MarkValue(rt, JS_MKPTR(JS_TAG_MODULE, m), mark_func); } JS_MarkValue(rt, ctx->global_obj, mark_func); @@ -2206,7 +2313,7 @@ static void JS_MarkContext(JSRuntime *rt, JSContext *ctx, for(i = 0; i < rt->class_count; i++) { JS_MarkValue(rt, ctx->class_proto[i], mark_func); } - JS_MarkValue(rt, ctx->iterator_proto, mark_func); + JS_MarkValue(rt, ctx->iterator_ctor, mark_func); JS_MarkValue(rt, ctx->async_iterator_proto, mark_func); JS_MarkValue(rt, ctx->promise_ctor, mark_func); JS_MarkValue(rt, ctx->array_ctor, mark_func); @@ -2270,7 +2377,7 @@ void JS_FreeContext(JSContext *ctx) JS_FreeValue(ctx, ctx->class_proto[i]); } js_free_rt(rt, ctx->class_proto); - JS_FreeValue(ctx, ctx->iterator_proto); + JS_FreeValue(ctx, ctx->iterator_ctor); JS_FreeValue(ctx, ctx->async_iterator_proto); JS_FreeValue(ctx, ctx->promise_ctor); JS_FreeValue(ctx, ctx->array_ctor); @@ -2537,7 +2644,7 @@ static int JS_InitAtoms(JSRuntime *rt) rt->atom_count = 0; rt->atom_size = 0; rt->atom_free_index = 0; - if (JS_ResizeAtomHash(rt, 256)) /* there are at least 195 predefined atoms */ + if (JS_ResizeAtomHash(rt, 512)) /* there are at least 504 predefined atoms */ return -1; p = js_atom_init; @@ -2682,9 +2789,9 @@ static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type) /* alloc new with size progression 3/2: 4 6 9 13 19 28 42 63 94 141 211 316 474 711 1066 1599 2398 3597 5395 8092 - preallocating space for predefined atoms (at least 195). + preallocating space for predefined atoms (at least 504). */ - new_size = max_int(211, rt->atom_size * 3 / 2); + new_size = max_int(711, rt->atom_size * 3 / 2); if (new_size > JS_ATOM_MAX) goto fail; /* XXX: should use realloc2 to use slack space */ @@ -3150,9 +3257,9 @@ static JSValue JS_AtomIsNumericIndex1(JSContext *ctx, JSAtom atom) JS_FreeValue(ctx, num); return str; } - ret = js_string_compare(ctx, p, JS_VALUE_GET_STRING(str)); + ret = js_string_eq(ctx, p, JS_VALUE_GET_STRING(str)); JS_FreeValue(ctx, str); - if (ret == 0) { + if (ret) { return num; } else { JS_FreeValue(ctx, num); @@ -3201,21 +3308,19 @@ static BOOL JS_AtomSymbolHasDescription(JSContext *ctx, JSAtom v) !(p->len == 0 && p->is_wide_char != 0)); } -static __maybe_unused void print_atom(JSContext *ctx, JSAtom atom) -{ - js_print_atom(ctx->rt, stdout, atom); -} - /* free with JS_FreeCString() */ -const char *JS_AtomToCString(JSContext *ctx, JSAtom atom) +const char *JS_AtomToCStringLen(JSContext *ctx, size_t *plen, JSAtom atom) { JSValue str; const char *cstr; str = JS_AtomToString(ctx, atom); - if (JS_IsException(str)) + if (JS_IsException(str)) { + if (plen) + *plen = 0; return NULL; - cstr = JS_ToCString(ctx, str); + } + cstr = JS_ToCStringLen(ctx, plen, str); JS_FreeValue(ctx, str); return cstr; } @@ -3546,7 +3651,7 @@ static no_inline int string_buffer_realloc(StringBuffer *s, int new_len, int c) return 0; } -static no_inline int string_buffer_putc_slow(StringBuffer *s, uint32_t c) +static no_inline int string_buffer_putc16_slow(StringBuffer *s, uint32_t c) { if (unlikely(s->len >= s->size)) { if (string_buffer_realloc(s, s->len + 1, c)) @@ -3591,11 +3696,10 @@ static int string_buffer_putc16(StringBuffer *s, uint32_t c) return 0; } } - return string_buffer_putc_slow(s, c); + return string_buffer_putc16_slow(s, c); } -/* 0 <= c <= 0x10ffff */ -static int string_buffer_putc(StringBuffer *s, uint32_t c) +static int string_buffer_putc_slow(StringBuffer *s, uint32_t c) { if (unlikely(c >= 0x10000)) { /* surrogate pair */ @@ -3606,6 +3710,27 @@ static int string_buffer_putc(StringBuffer *s, uint32_t c) return string_buffer_putc16(s, c); } +/* 0 <= c <= 0x10ffff */ +static inline int string_buffer_putc(StringBuffer *s, uint32_t c) +{ + if (likely(s->len < s->size)) { + if (s->is_wide_char) { + if (c < 0x10000) { + s->str->u.str16[s->len++] = c; + return 0; + } else if (likely((s->len + 1) < s->size)) { + s->str->u.str16[s->len++] = get_hi_surrogate(c); + s->str->u.str16[s->len++] = get_lo_surrogate(c); + return 0; + } + } else if (c < 0x100) { + s->str->u.str8[s->len++] = c; + return 0; + } + } + return string_buffer_putc_slow(s, c); +} + static int string_getc(const JSString *p, int *pidx) { int idx, c, c1; @@ -4036,6 +4161,16 @@ static int js_string_memcmp(const JSString *p1, int pos1, const JSString *p2, return res; } +static BOOL js_string_eq(JSContext *ctx, + const JSString *p1, const JSString *p2) +{ + if (p1->len != p2->len) + return FALSE; + if (p1 == p2) + return TRUE; + return js_string_memcmp(p1, 0, p2, 0, p1->len) == 0; +} + /* return < 0, 0 or > 0 */ static int js_string_compare(JSContext *ctx, const JSString *p1, const JSString *p2) @@ -4649,19 +4784,14 @@ static void js_shape_hash_unlink(JSRuntime *rt, JSShape *sh) rt->shape_hash_count--; } -/* create a new empty shape with prototype 'proto' */ -static no_inline JSShape *js_new_shape2(JSContext *ctx, JSObject *proto, - int hash_size, int prop_size) +/* create a new empty shape with prototype 'proto'. It is not hashed */ +static inline JSShape *js_new_shape_nohash(JSContext *ctx, JSObject *proto, + int hash_size, int prop_size) { JSRuntime *rt = ctx->rt; void *sh_alloc; JSShape *sh; - /* resize the shape hash table if necessary */ - if (2 * (rt->shape_hash_count + 1) > rt->shape_hash_size) { - resize_shape_hash(rt, rt->shape_hash_bits + 1); - } - sh_alloc = js_malloc(ctx, get_shape_size(hash_size, prop_size)); if (!sh_alloc) return NULL; @@ -4677,11 +4807,29 @@ static no_inline JSShape *js_new_shape2(JSContext *ctx, JSObject *proto, sh->prop_size = prop_size; sh->prop_count = 0; sh->deleted_prop_count = 0; + sh->is_hashed = FALSE; + return sh; +} +/* create a new empty shape with prototype 'proto' */ +static no_inline JSShape *js_new_shape2(JSContext *ctx, JSObject *proto, + int hash_size, int prop_size) +{ + JSRuntime *rt = ctx->rt; + JSShape *sh; + + /* resize the shape hash table if necessary */ + if (2 * (rt->shape_hash_count + 1) > rt->shape_hash_size) { + resize_shape_hash(rt, rt->shape_hash_bits + 1); + } + + sh = js_new_shape_nohash(ctx, proto, hash_size, prop_size); + if (!sh) + return NULL; + /* insert in the hash table */ sh->hash = shape_initial_hash(proto); sh->is_hashed = TRUE; - sh->has_small_array_index = FALSE; js_shape_hash_link(ctx->rt, sh); return sh; } @@ -4926,7 +5074,6 @@ static int add_shape_property(JSContext *ctx, JSShape **psh, pr = &prop[sh->prop_count++]; pr->atom = JS_DupAtom(ctx, atom); pr->flags = prop_flags; - sh->has_small_array_index |= __JS_AtomIsTaggedInt(atom); /* add in hash table */ hash_mask = sh->prop_hash_mask; h = atom & hash_mask; @@ -5041,6 +5188,7 @@ static JSValue JS_NewObjectFromShape(JSContext *ctx, JSShape *sh, JSClassID clas if (unlikely(!p)) goto fail; p->class_id = class_id; + p->is_prototype = 0; p->extensible = TRUE; p->free_mark = 0; p->is_exotic = 0; @@ -5096,6 +5244,7 @@ static JSValue JS_NewObjectFromShape(JSContext *ctx, JSShape *sh, JSClassID clas case JS_CLASS_UINT32_ARRAY: case JS_CLASS_BIG_INT64_ARRAY: case JS_CLASS_BIG_UINT64_ARRAY: + case JS_CLASS_FLOAT16_ARRAY: case JS_CLASS_FLOAT32_ARRAY: case JS_CLASS_FLOAT64_ARRAY: p->is_exotic = 1; @@ -5118,7 +5267,10 @@ static JSValue JS_NewObjectFromShape(JSContext *ctx, JSShape *sh, JSClassID clas case JS_CLASS_REGEXP: p->u.regexp.pattern = NULL; p->u.regexp.bytecode = NULL; - goto set_exotic; + break; + case JS_CLASS_GLOBAL_OBJECT: + p->u.global_object.uninitialized_vars = JS_UNDEFINED; + break; default: set_exotic: if (ctx->rt->class_array[class_id].exotic) { @@ -5158,6 +5310,29 @@ JSValue JS_NewObjectProtoClass(JSContext *ctx, JSValueConst proto_val, return JS_NewObjectFromShape(ctx, sh, class_id); } +/* WARNING: the shape is not hashed. It is used for objects where + factorizing the shape is not relevant (prototypes, constructors) */ +static JSValue JS_NewObjectProtoClassAlloc(JSContext *ctx, JSValueConst proto_val, + JSClassID class_id, int n_alloc_props) +{ + JSShape *sh; + JSObject *proto; + int hash_size, hash_bits; + + if (n_alloc_props <= JS_PROP_INITIAL_SIZE) { + n_alloc_props = JS_PROP_INITIAL_SIZE; + hash_size = JS_PROP_INITIAL_HASH_SIZE; + } else { + hash_bits = 32 - clz32(n_alloc_props - 1); /* ceil(log2(radix)) */ + hash_size = 1 << hash_bits; + } + proto = get_proto_obj(proto_val); + sh = js_new_shape_nohash(ctx, proto, hash_size, n_alloc_props); + if (!sh) + return JS_EXCEPTION; + return JS_NewObjectFromShape(ctx, sh, class_id); +} + #if 0 static JSValue JS_GetObjectData(JSContext *ctx, JSValueConst obj) { @@ -5321,13 +5496,17 @@ static int js_method_set_properties(JSContext *ctx, JSValueConst func_obj, static JSValue JS_NewCFunction3(JSContext *ctx, JSCFunction *func, const char *name, int length, JSCFunctionEnum cproto, int magic, - JSValueConst proto_val) + JSValueConst proto_val, int n_fields) { JSValue func_obj; JSObject *p; JSAtom name_atom; - func_obj = JS_NewObjectProtoClass(ctx, proto_val, JS_CLASS_C_FUNCTION); + if (n_fields > 0) { + func_obj = JS_NewObjectProtoClassAlloc(ctx, proto_val, JS_CLASS_C_FUNCTION, n_fields); + } else { + func_obj = JS_NewObjectProtoClass(ctx, proto_val, JS_CLASS_C_FUNCTION); + } if (JS_IsException(func_obj)) return func_obj; p = JS_VALUE_GET_OBJ(func_obj); @@ -5343,6 +5522,10 @@ static JSValue JS_NewCFunction3(JSContext *ctx, JSCFunction *func, if (!name) name = ""; name_atom = JS_NewAtom(ctx, name); + if (name_atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, func_obj); + return JS_EXCEPTION; + } js_function_set_properties(ctx, func_obj, name_atom, length); JS_FreeAtom(ctx, name_atom); return func_obj; @@ -5354,7 +5537,7 @@ JSValue JS_NewCFunction2(JSContext *ctx, JSCFunction *func, int length, JSCFunctionEnum cproto, int magic) { return JS_NewCFunction3(ctx, func, name, length, cproto, magic, - ctx->function_proto); + ctx->function_proto, 0); } typedef struct JSCFunctionDataRecord { @@ -5768,6 +5951,9 @@ static void free_gc_object(JSRuntime *rt, JSGCObjectHeader *gp) case JS_GC_OBJ_TYPE_ASYNC_FUNCTION: __async_func_free(rt, (JSAsyncFunctionState *)gp); break; + case JS_GC_OBJ_TYPE_MODULE: + js_free_module_def(rt, (JSModuleDef *)gp); + break; default: abort(); } @@ -5832,6 +6018,7 @@ void __JS_FreeValueRT(JSRuntime *rt, JSValue v) break; case JS_TAG_OBJECT: case JS_TAG_FUNCTION_BYTECODE: + case JS_TAG_MODULE: { JSGCObjectHeader *p = JS_VALUE_GET_PTR(v); if (rt->gc_phase != JS_GC_PHASE_REMOVE_CYCLES) { @@ -5844,9 +6031,6 @@ void __JS_FreeValueRT(JSRuntime *rt, JSValue v) } } break; - case JS_TAG_MODULE: - abort(); /* never freed here */ - break; case JS_TAG_BIG_INT: { JSBigInt *p = JS_VALUE_GET_PTR(v); @@ -5920,6 +6104,7 @@ void JS_MarkValue(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) switch(JS_VALUE_GET_TAG(val)) { case JS_TAG_OBJECT: case JS_TAG_FUNCTION_BYTECODE: + case JS_TAG_MODULE: mark_func(rt, JS_VALUE_GET_PTR(val)); break; default: @@ -6032,6 +6217,12 @@ static void mark_children(JSRuntime *rt, JSGCObjectHeader *gp, JS_MarkContext(rt, ctx, mark_func); } break; + case JS_GC_OBJ_TYPE_MODULE: + { + JSModuleDef *m = (JSModuleDef *)gp; + js_mark_module_def(rt, m, mark_func); + } + break; default: abort(); } @@ -6128,6 +6319,7 @@ static void gc_free_cycles(JSRuntime *rt) case JS_GC_OBJ_TYPE_JS_OBJECT: case JS_GC_OBJ_TYPE_FUNCTION_BYTECODE: case JS_GC_OBJ_TYPE_ASYNC_FUNCTION: + case JS_GC_OBJ_TYPE_MODULE: #ifdef DUMP_GC_FREE if (!header_done) { printf("Freeing cycles:\n"); @@ -6150,7 +6342,8 @@ static void gc_free_cycles(JSRuntime *rt) p = list_entry(el, JSGCObjectHeader, link); assert(p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT || p->gc_obj_type == JS_GC_OBJ_TYPE_FUNCTION_BYTECODE || - p->gc_obj_type == JS_GC_OBJ_TYPE_ASYNC_FUNCTION); + p->gc_obj_type == JS_GC_OBJ_TYPE_ASYNC_FUNCTION || + p->gc_obj_type == JS_GC_OBJ_TYPE_MODULE); if (p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT && ((JSObject *)p)->weakref_count != 0) { /* keep the object because there are weak references to it */ @@ -6487,6 +6680,7 @@ void JS_ComputeMemoryUsage(JSRuntime *rt, JSMemoryUsage *s) case JS_CLASS_UINT32_ARRAY: /* u.typed_array / u.array */ case JS_CLASS_BIG_INT64_ARRAY: /* u.typed_array / u.array */ case JS_CLASS_BIG_UINT64_ARRAY: /* u.typed_array / u.array */ + case JS_CLASS_FLOAT16_ARRAY: /* u.typed_array / u.array */ case JS_CLASS_FLOAT32_ARRAY: /* u.typed_array / u.array */ case JS_CLASS_FLOAT64_ARRAY: /* u.typed_array / u.array */ case JS_CLASS_DATAVIEW: /* u.typed_array */ @@ -6835,20 +7029,30 @@ static int find_line_num(JSContext *ctx, JSFunctionBytecode *b, return 0; } -/* in order to avoid executing arbitrary code during the stack trace - generation, we only look at simple 'name' properties containing a - string. */ -static const char *get_func_name(JSContext *ctx, JSValueConst func) +/* return a string property without executing arbitrary JS code (used + when dumping the stack trace or in debug print). */ +static const char *get_prop_string(JSContext *ctx, JSValueConst obj, JSAtom prop) { + JSObject *p; JSProperty *pr; JSShapeProperty *prs; JSValueConst val; - if (JS_VALUE_GET_TAG(func) != JS_TAG_OBJECT) - return NULL; - prs = find_own_property(&pr, JS_VALUE_GET_OBJ(func), JS_ATOM_name); - if (!prs) + if (JS_VALUE_GET_TAG(obj) != JS_TAG_OBJECT) return NULL; + p = JS_VALUE_GET_OBJ(obj); + prs = find_own_property(&pr, p, prop); + if (!prs) { + /* we look at one level in the prototype to handle the 'name' + field of the Error objects */ + p = p->shape->proto; + if (!p) + return NULL; + prs = find_own_property(&pr, p, prop); + if (!prs) + return NULL; + } + if ((prs->flags & JS_PROP_TMASK) != JS_PROP_NORMAL) return NULL; val = pr->u.value; @@ -6872,6 +7076,9 @@ static void build_backtrace(JSContext *ctx, JSValueConst error_obj, const char *str1; JSObject *p; + if (!JS_IsObject(error_obj)) + return; /* protection in the out of memory case */ + js_dbuf_init(ctx, &dbuf); if (filename) { dbuf_printf(&dbuf, " at %s", filename); @@ -6879,13 +7086,17 @@ static void build_backtrace(JSContext *ctx, JSValueConst error_obj, dbuf_printf(&dbuf, ":%d:%d", line_num, col_num); dbuf_putc(&dbuf, '\n'); str = JS_NewString(ctx, filename); + if (JS_IsException(str)) + return; /* Note: SpiderMonkey does that, could update once there is a standard */ - JS_DefinePropertyValue(ctx, error_obj, JS_ATOM_fileName, str, - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - JS_DefinePropertyValue(ctx, error_obj, JS_ATOM_lineNumber, JS_NewInt32(ctx, line_num), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - JS_DefinePropertyValue(ctx, error_obj, JS_ATOM_columnNumber, JS_NewInt32(ctx, col_num), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + if (JS_DefinePropertyValue(ctx, error_obj, JS_ATOM_fileName, str, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0 || + JS_DefinePropertyValue(ctx, error_obj, JS_ATOM_lineNumber, JS_NewInt32(ctx, line_num), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0 || + JS_DefinePropertyValue(ctx, error_obj, JS_ATOM_columnNumber, JS_NewInt32(ctx, col_num), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0) { + return; + } } for(sf = ctx->rt->current_stack_frame; sf != NULL; sf = sf->prev_frame) { if (sf->js_mode & JS_MODE_BACKTRACE_BARRIER) @@ -6894,7 +7105,7 @@ static void build_backtrace(JSContext *ctx, JSValueConst error_obj, backtrace_flags &= ~JS_BACKTRACE_FLAG_SKIP_FIRST_LEVEL; continue; } - func_name_str = get_func_name(ctx, sf->cur_func); + func_name_str = get_prop_string(ctx, sf->cur_func, JS_ATOM_name); if (!func_name_str || func_name_str[0] == '\0') str1 = ""; else @@ -6970,9 +7181,9 @@ static JSValue JS_ThrowError2(JSContext *ctx, JSErrorEnum error_num, JS_DefinePropertyValue(ctx, obj, JS_ATOM_message, JS_NewString(ctx, buf), JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - } - if (add_backtrace) { - build_backtrace(ctx, obj, NULL, 0, 0, 0); + if (add_backtrace) { + build_backtrace(ctx, obj, NULL, 0, 0, 0); + } } ret = JS_Throw(ctx, obj); return ret; @@ -7115,6 +7326,22 @@ static JSValue JS_ThrowTypeErrorNotAnObject(JSContext *ctx) return JS_ThrowTypeError(ctx, "not an object"); } +static JSValue JS_ThrowTypeErrorNotAConstructor(JSContext *ctx, + JSValueConst func_obj) +{ + const char *name; + if (!JS_IsFunction(ctx, func_obj)) + goto fail; + name = get_prop_string(ctx, func_obj, JS_ATOM_name); + if (!name) { + fail: + return JS_ThrowTypeError(ctx, "not a constructor"); + } + JS_ThrowTypeError(ctx, "%s is not a constructor", name); + JS_FreeCString(ctx, name); + return JS_EXCEPTION; +} + static JSValue JS_ThrowTypeErrorNotASymbol(JSContext *ctx) { return JS_ThrowTypeError(ctx, "not a symbol"); @@ -7284,6 +7511,14 @@ static int JS_SetPrototypeInternal(JSContext *ctx, JSValueConst obj, if (sh->proto) JS_FreeValue(ctx, JS_MKPTR(JS_TAG_OBJECT, sh->proto)); sh->proto = proto; + if (proto) + proto->is_prototype = TRUE; + if (p->is_prototype) { + /* track modification of Array.prototype */ + if (unlikely(p == JS_VALUE_GET_OBJ(ctx->class_proto[JS_CLASS_ARRAY]))) { + ctx->std_array_prototype = FALSE; + } + } return TRUE; } @@ -7493,6 +7728,16 @@ static int JS_AutoInitProperty(JSContext *ctx, JSObject *p, JSAtom prop, prs->flags |= JS_PROP_VARREF; pr->u.var_ref = JS_VALUE_GET_PTR(val); pr->u.var_ref->header.ref_count++; + } else if (p->class_id == JS_CLASS_GLOBAL_OBJECT) { + JSVarRef *var_ref; + /* in the global object we use references */ + var_ref = js_create_var_ref(ctx, FALSE); + if (!var_ref) + return -1; + prs->flags |= JS_PROP_VARREF; + pr->u.var_ref = var_ref; + var_ref->value = val; + var_ref->is_const = !(prs->flags & JS_PROP_WRITABLE); } else { pr->u.value = val; } @@ -7875,7 +8120,7 @@ static int num_keys_cmp(const void *p1, const void *p2, void *opaque) return 1; } -static void js_free_prop_enum(JSContext *ctx, JSPropertyEnum *tab, uint32_t len) +void JS_FreePropertyEnum(JSContext *ctx, JSPropertyEnum *tab, uint32_t len) { uint32_t i; if (tab) { @@ -7969,7 +8214,7 @@ static int __exception JS_GetOwnPropertyNamesInternal(JSContext *ctx, /* set the "is_enumerable" field if necessary */ res = JS_GetOwnPropertyInternal(ctx, &desc, p, atom); if (res < 0) { - js_free_prop_enum(ctx, tab_exotic, exotic_count); + JS_FreePropertyEnum(ctx, tab_exotic, exotic_count); return -1; } if (res) { @@ -8000,7 +8245,7 @@ static int __exception JS_GetOwnPropertyNamesInternal(JSContext *ctx, if (atom_count < exotic_keys_count || atom_count > INT32_MAX) { add_overflow: JS_ThrowOutOfMemory(ctx); - js_free_prop_enum(ctx, tab_exotic, exotic_count); + JS_FreePropertyEnum(ctx, tab_exotic, exotic_count); return -1; } /* XXX: need generic way to test for js_malloc(ctx, a * b) overflow */ @@ -8008,7 +8253,7 @@ static int __exception JS_GetOwnPropertyNamesInternal(JSContext *ctx, /* avoid allocating 0 bytes */ tab_atom = js_malloc(ctx, sizeof(tab_atom[0]) * max_int(atom_count, 1)); if (!tab_atom) { - js_free_prop_enum(ctx, tab_exotic, exotic_count); + JS_FreePropertyEnum(ctx, tab_exotic, exotic_count); return -1; } @@ -8053,7 +8298,7 @@ static int __exception JS_GetOwnPropertyNamesInternal(JSContext *ctx, for(i = 0; i < len; i++) { tab_atom[num_index].atom = __JS_AtomFromUInt32(i); if (tab_atom[num_index].atom == JS_ATOM_NULL) { - js_free_prop_enum(ctx, tab_atom, num_index); + JS_FreePropertyEnum(ctx, tab_atom, num_index); return -1; } tab_atom[num_index].is_enumerable = TRUE; @@ -8220,9 +8465,21 @@ int JS_PreventExtensions(JSContext *ctx, JSValueConst obj) return FALSE; p = JS_VALUE_GET_OBJ(obj); if (unlikely(p->is_exotic)) { - const JSClassExoticMethods *em = ctx->rt->class_array[p->class_id].exotic; - if (em && em->prevent_extensions) { - return em->prevent_extensions(ctx, obj); + if (p->class_id >= JS_CLASS_UINT8C_ARRAY && + p->class_id <= JS_CLASS_FLOAT64_ARRAY) { + JSTypedArray *ta; + JSArrayBuffer *abuf; + /* resizable type arrays return FALSE */ + ta = p->u.typed_array; + abuf = ta->buffer->u.array_buffer; + if (ta->track_rab || + (array_buffer_is_resizable(abuf) && !abuf->shared)) + return FALSE; + } else { + const JSClassExoticMethods *em = ctx->rt->class_array[p->class_id].exotic; + if (em && em->prevent_extensions) { + return em->prevent_extensions(ctx, obj); + } } } p->extensible = FALSE; @@ -8349,6 +8606,9 @@ static JSValue JS_GetPropertyValue(JSContext *ctx, JSValueConst this_obj, case JS_CLASS_BIG_UINT64_ARRAY: if (unlikely(idx >= p->u.array.count)) goto slow_path; return JS_NewBigUint64(ctx, p->u.array.u.uint64_ptr[idx]); + case JS_CLASS_FLOAT16_ARRAY: + if (unlikely(idx >= p->u.array.count)) goto slow_path; + return __JS_NewFloat64(ctx, fromfp16(p->u.array.u.fp16_ptr[idx])); case JS_CLASS_FLOAT32_ARRAY: if (unlikely(idx >= p->u.array.count)) goto slow_path; return __JS_NewFloat64(ctx, p->u.array.u.float_ptr[idx]); @@ -8441,6 +8701,8 @@ JSValue JS_GetPropertyStr(JSContext *ctx, JSValueConst this_obj, JSAtom atom; JSValue ret; atom = JS_NewAtom(ctx, prop); + if (atom == JS_ATOM_NULL) + return JS_EXCEPTION; ret = JS_GetProperty(ctx, this_obj, atom); JS_FreeAtom(ctx, atom); return ret; @@ -8453,6 +8715,14 @@ static JSProperty *add_property(JSContext *ctx, { JSShape *sh, *new_sh; + if (unlikely(p->is_prototype)) { + /* track addition of small integer properties to Array.prototype and Object.prototype */ + if (unlikely((p == JS_VALUE_GET_OBJ(ctx->class_proto[JS_CLASS_ARRAY]) || + p == JS_VALUE_GET_OBJ(ctx->class_proto[JS_CLASS_OBJECT])) && + __JS_AtomIsTaggedInt(prop))) { + ctx->std_array_prototype = FALSE; + } + } sh = p->shape; if (sh->is_hashed) { /* try to find an existing shape */ @@ -8522,6 +8792,34 @@ static no_inline __exception int convert_fast_array_to_array(JSContext *ctx, p->u.array.u.values = NULL; /* fail safe */ p->u.array.u1.size = 0; p->fast_array = 0; + + /* track modification of Array.prototype */ + if (unlikely(p == JS_VALUE_GET_OBJ(ctx->class_proto[JS_CLASS_ARRAY]))) { + ctx->std_array_prototype = FALSE; + } + return 0; +} + +static int remove_global_object_property(JSContext *ctx, JSObject *p, + JSShapeProperty *prs, JSProperty *pr) +{ + JSVarRef *var_ref; + JSObject *p1; + JSProperty *pr1; + + var_ref = pr->u.var_ref; + if (var_ref->header.ref_count == 1) + return 0; + p1 = JS_VALUE_GET_OBJ(p->u.global_object.uninitialized_vars); + pr1 = add_property(ctx, p1, prs->atom, JS_PROP_C_W_E | JS_PROP_VARREF); + if (!pr1) + return -1; + pr1->u.var_ref = var_ref; + var_ref->header.ref_count++; + JS_FreeValue(ctx, var_ref->value); + var_ref->is_lexical = FALSE; + var_ref->is_const = FALSE; + var_ref->value = JS_UNINITIALIZED; return 0; } @@ -8562,6 +8860,11 @@ static int delete_property(JSContext *ctx, JSObject *p, JSAtom atom) sh->deleted_prop_count++; /* free the entry */ pr1 = &p->prop[h - 1]; + if (unlikely(p->class_id == JS_CLASS_GLOBAL_OBJECT)) { + if ((pr->flags & JS_PROP_TMASK) == JS_PROP_VARREF) + if (remove_global_object_property(ctx, p, pr, pr1)) + return -1; + } free_property(ctx->rt, pr1, pr->flags); JS_FreeAtom(ctx, pr->atom); /* put default values */ @@ -8746,8 +9049,8 @@ static int expand_fast_array(JSContext *ctx, JSObject *p, uint32_t new_len) /* Preconditions: 'p' must be of class JS_CLASS_ARRAY, p->fast_array = TRUE and p->extensible = TRUE */ -static int add_fast_array_element(JSContext *ctx, JSObject *p, - JSValue val, int flags) +static inline int add_fast_array_element(JSContext *ctx, JSObject *p, + JSValue val, int flags) { uint32_t new_len, array_len; /* extend the array by one */ @@ -8800,6 +9103,57 @@ static JSValue js_allocate_fast_array(JSContext *ctx, int64_t len) return arr; } +static JSValue js_create_array(JSContext *ctx, int len, JSValueConst *tab) +{ + JSValue obj; + JSObject *p; + int i; + + obj = JS_NewArray(ctx); + if (JS_IsException(obj)) + return JS_EXCEPTION; + if (len > 0) { + p = JS_VALUE_GET_OBJ(obj); + if (expand_fast_array(ctx, p, len) < 0) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + p->u.array.count = len; + for(i = 0; i < len; i++) + p->u.array.u.values[i] = JS_DupValue(ctx, tab[i]); + /* update the 'length' field */ + set_value(ctx, &p->prop[0].u.value, JS_NewInt32(ctx, len)); + } + return obj; +} + +static JSValue js_create_array_free(JSContext *ctx, int len, JSValue *tab) +{ + JSValue obj; + JSObject *p; + int i; + + obj = JS_NewArray(ctx); + if (JS_IsException(obj)) + goto fail; + if (len > 0) { + p = JS_VALUE_GET_OBJ(obj); + if (expand_fast_array(ctx, p, len) < 0) { + JS_FreeValue(ctx, obj); + fail: + for(i = 0; i < len; i++) + JS_FreeValue(ctx, tab[i]); + return JS_EXCEPTION; + } + p->u.array.count = len; + for(i = 0; i < len; i++) + p->u.array.u.values[i] = tab[i]; + /* update the 'length' field */ + set_value(ctx, &p->prop[0].u.value, JS_NewInt32(ctx, len)); + } + return obj; +} + static void js_free_desc(JSContext *ctx, JSPropertyDescriptor *desc) { JS_FreeValue(ctx, desc->getter); @@ -8808,11 +9162,9 @@ static void js_free_desc(JSContext *ctx, JSPropertyDescriptor *desc) } /* return -1 in case of exception or TRUE or FALSE. Warning: 'val' is - freed by the function. 'flags' is a bitmask of JS_PROP_NO_ADD, - JS_PROP_THROW or JS_PROP_THROW_STRICT. If JS_PROP_NO_ADD is set, - the new property is not added and an error is raised. 'this_obj' is - the receiver. If obj != this_obj, then obj must be an object - (Reflect.set case). */ + freed by the function. 'flags' is a bitmask of JS_PROP_THROW and + JS_PROP_THROW_STRICT. 'this_obj' is the receiver. If obj != + this_obj, then obj must be an object (Reflect.set case). */ int JS_SetPropertyInternal(JSContext *ctx, JSValueConst obj, JSAtom prop, JSValue val, JSValueConst this_obj, int flags) { @@ -8871,10 +9223,9 @@ int JS_SetPropertyInternal(JSContext *ctx, JSValueConst obj, } else if ((prs->flags & JS_PROP_TMASK) == JS_PROP_GETSET) { return call_setter(ctx, pr->u.getset.setter, this_obj, val, flags); } else if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) { - /* JS_PROP_WRITABLE is always true for variable - references, but they are write protected in module name - spaces. */ - if (p->class_id == JS_CLASS_MODULE_NS) + /* XXX: already use var_ref->is_const. Cannot simplify use the + writable flag for JS_CLASS_MODULE_NS. */ + if (p->class_id == JS_CLASS_MODULE_NS || pr->u.var_ref->is_const) goto read_only_prop; set_value(ctx, pr->u.var_ref->pvalue, val); return TRUE; @@ -9008,12 +9359,6 @@ int JS_SetPropertyInternal(JSContext *ctx, JSValueConst obj, } } - if (unlikely(flags & JS_PROP_NO_ADD)) { - JS_FreeValue(ctx, val); - JS_ThrowReferenceErrorNotDefined(ctx, prop); - return -1; - } - if (unlikely(!p)) { JS_FreeValue(ctx, val); return JS_ThrowTypeErrorOrFalse(ctx, flags, "not an object"); @@ -9039,6 +9384,8 @@ int JS_SetPropertyInternal(JSContext *ctx, JSValueConst obj, goto generic_create_prop; } } else { + if (unlikely(p->class_id == JS_CLASS_GLOBAL_OBJECT)) + goto generic_create_prop; pr = add_property(ctx, p, prop, JS_PROP_C_W_E); if (unlikely(!pr)) { JS_FreeValue(ctx, val); @@ -9106,27 +9453,13 @@ static int JS_SetPropertyValue(JSContext *ctx, JSValueConst this_obj, switch(p->class_id) { case JS_CLASS_ARRAY: if (unlikely(idx >= (uint32_t)p->u.array.count)) { - JSObject *p1; - JSShape *sh1; - /* fast path to add an element to the array */ - if (idx != (uint32_t)p->u.array.count || - !p->fast_array || !p->extensible) + if (unlikely(idx != (uint32_t)p->u.array.count || + !p->fast_array || + !p->extensible || + p->shape->proto != JS_VALUE_GET_OBJ(ctx->class_proto[JS_CLASS_ARRAY]) || + !ctx->std_array_prototype)) { goto slow_path; - /* check if prototype chain has a numeric property */ - p1 = p->shape->proto; - while (p1 != NULL) { - sh1 = p1->shape; - if (p1->class_id == JS_CLASS_ARRAY) { - if (unlikely(!p1->fast_array)) - goto slow_path; - } else if (p1->class_id == JS_CLASS_OBJECT) { - if (unlikely(sh1->has_small_array_index)) - goto slow_path; - } else { - goto slow_path; - } - p1 = sh1->proto; } /* add element */ return add_fast_array_element(ctx, p, val, flags); @@ -9183,6 +9516,13 @@ static int JS_SetPropertyValue(JSContext *ctx, JSValueConst this_obj, p->u.array.u.uint64_ptr[idx] = v; } break; + case JS_CLASS_FLOAT16_ARRAY: + if (JS_ToFloat64Free(ctx, &d, val)) + return -1; + if (unlikely(idx >= (uint32_t)p->u.array.count)) + goto ta_out_of_bound; + p->u.array.u.fp16_ptr[idx] = tofp16(d); + break; case JS_CLASS_FLOAT32_ARRAY: if (JS_ToFloat64Free(ctx, &d, val)) return -1; @@ -9253,6 +9593,10 @@ int JS_SetPropertyStr(JSContext *ctx, JSValueConst this_obj, JSAtom atom; int ret; atom = JS_NewAtom(ctx, prop); + if (atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, val); + return -1; + } ret = JS_SetPropertyInternal(ctx, this_obj, atom, val, this_obj, JS_PROP_THROW); JS_FreeAtom(ctx, atom); return ret; @@ -9276,7 +9620,9 @@ static int JS_CreateProperty(JSContext *ctx, JSObject *p, { JSProperty *pr; int ret, prop_flags; - + JSVarRef *var_ref; + JSObject *delete_obj; + /* add a new property or modify an existing exotic one */ if (p->is_exotic) { if (p->class_id == JS_CLASS_ARRAY) { @@ -9351,15 +9697,37 @@ static int JS_CreateProperty(JSContext *ctx, JSObject *p, return JS_ThrowTypeErrorOrFalse(ctx, flags, "object is not extensible"); } + var_ref = NULL; + delete_obj = NULL; if (flags & (JS_PROP_HAS_GET | JS_PROP_HAS_SET)) { prop_flags = (flags & (JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE)) | JS_PROP_GETSET; } else { prop_flags = flags & JS_PROP_C_W_E; + if (p->class_id == JS_CLASS_GLOBAL_OBJECT) { + JSObject *p1 = JS_VALUE_GET_OBJ(p->u.global_object.uninitialized_vars); + JSShapeProperty *prs1; + JSProperty *pr1; + prs1 = find_own_property(&pr1, p1, prop); + if (prs1) { + delete_obj = p1; + var_ref = pr1->u.var_ref; + var_ref->header.ref_count++; + } else { + var_ref = js_create_var_ref(ctx, FALSE); + if (!var_ref) + return -1; + } + var_ref->is_const = !(prop_flags & JS_PROP_WRITABLE); + prop_flags |= JS_PROP_VARREF; + } } pr = add_property(ctx, p, prop, prop_flags); - if (unlikely(!pr)) + if (unlikely(!pr)) { + if (var_ref) + free_var_ref(ctx->rt, var_ref); return -1; + } if (flags & (JS_PROP_HAS_GET | JS_PROP_HAS_SET)) { pr->u.getset.getter = NULL; if ((flags & JS_PROP_HAS_GET) && JS_IsFunction(ctx, getter)) { @@ -9371,6 +9739,15 @@ static int JS_CreateProperty(JSContext *ctx, JSObject *p, pr->u.getset.setter = JS_VALUE_GET_OBJ(JS_DupValue(ctx, setter)); } + } else if (p->class_id == JS_CLASS_GLOBAL_OBJECT) { + if (delete_obj) + delete_property(ctx, delete_obj, prop); + pr->u.var_ref = var_ref; + if (flags & JS_PROP_HAS_VALUE) { + *var_ref->pvalue = JS_DupValue(ctx, val); + } else { + *var_ref->pvalue = JS_UNDEFINED; + } } else { if (flags & JS_PROP_HAS_VALUE) { pr->u.value = JS_DupValue(ctx, val); @@ -9525,6 +9902,10 @@ int JS_DefineProperty(JSContext *ctx, JSValueConst this_obj, return -1; /* convert to getset */ if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) { + if (unlikely(p->class_id == JS_CLASS_GLOBAL_OBJECT)) { + if (remove_global_object_property(ctx, p, prs, pr)) + return -1; + } free_var_ref(ctx->rt, pr->u.var_ref); } else { JS_FreeValue(ctx, pr->u.value); @@ -9563,14 +9944,31 @@ int JS_DefineProperty(JSContext *ctx, JSValueConst this_obj, } else { if ((prs->flags & JS_PROP_TMASK) == JS_PROP_GETSET) { /* convert to data descriptor */ - if (js_shape_prepare_update(ctx, p, &prs)) + JSVarRef *var_ref; + if (unlikely(p->class_id == JS_CLASS_GLOBAL_OBJECT)) { + var_ref = js_global_object_find_uninitialized_var(ctx, p, prop, FALSE); + if (!var_ref) + return -1; + } else { + var_ref = NULL; + } + if (js_shape_prepare_update(ctx, p, &prs)) { + if (var_ref) + free_var_ref(ctx->rt, var_ref); return -1; + } if (pr->u.getset.getter) JS_FreeValue(ctx, JS_MKPTR(JS_TAG_OBJECT, pr->u.getset.getter)); if (pr->u.getset.setter) JS_FreeValue(ctx, JS_MKPTR(JS_TAG_OBJECT, pr->u.getset.setter)); - prs->flags &= ~(JS_PROP_TMASK | JS_PROP_WRITABLE); - pr->u.value = JS_UNDEFINED; + if (var_ref) { + prs->flags = (prs->flags & ~JS_PROP_TMASK) | + JS_PROP_VARREF | JS_PROP_WRITABLE; + pr->u.var_ref = var_ref; + } else { + prs->flags &= ~(JS_PROP_TMASK | JS_PROP_WRITABLE); + pr->u.value = JS_UNDEFINED; + } } else if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) { /* Note: JS_PROP_VARREF is always writable */ } else { @@ -9597,8 +9995,6 @@ int JS_DefineProperty(JSContext *ctx, JSValueConst this_obj, JS_DupValue(ctx, val)); } } - /* if writable is set to false, no longer a - reference (for mapped arguments) */ if ((flags & (JS_PROP_HAS_WRITABLE | JS_PROP_WRITABLE)) == JS_PROP_HAS_WRITABLE) { JSValue val1; if (p->class_id == JS_CLASS_MODULE_NS) { @@ -9606,10 +10002,17 @@ int JS_DefineProperty(JSContext *ctx, JSValueConst this_obj, } if (js_shape_prepare_update(ctx, p, &prs)) return -1; - val1 = JS_DupValue(ctx, *pr->u.var_ref->pvalue); - free_var_ref(ctx->rt, pr->u.var_ref); - pr->u.value = val1; - prs->flags &= ~(JS_PROP_TMASK | JS_PROP_WRITABLE); + if (p->class_id == JS_CLASS_GLOBAL_OBJECT) { + pr->u.var_ref->is_const = TRUE; /* mark as read-only */ + prs->flags &= ~JS_PROP_WRITABLE; + } else { + /* if writable is set to false, no longer a + reference (for mapped arguments) */ + val1 = JS_DupValue(ctx, *pr->u.var_ref->pvalue); + free_var_ref(ctx->rt, pr->u.var_ref); + pr->u.value = val1; + prs->flags &= ~(JS_PROP_TMASK | JS_PROP_WRITABLE); + } } } else if (prs->flags & JS_PROP_LENGTH) { if (flags & JS_PROP_HAS_VALUE) { @@ -9809,6 +10212,10 @@ int JS_DefinePropertyValueStr(JSContext *ctx, JSValueConst this_obj, JSAtom atom; int ret; atom = JS_NewAtom(ctx, prop); + if (atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, val); + return -1; + } ret = JS_DefinePropertyValue(ctx, this_obj, atom, val, flags); JS_FreeAtom(ctx, atom); return ret; @@ -9938,83 +10345,6 @@ static int JS_CheckDefineGlobalVar(JSContext *ctx, JSAtom prop, int flags) return 0; } -/* def_flags is (0, DEFINE_GLOBAL_LEX_VAR) | - JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE */ -/* XXX: could support exotic global object. */ -static int JS_DefineGlobalVar(JSContext *ctx, JSAtom prop, int def_flags) -{ - JSObject *p; - JSShapeProperty *prs; - JSProperty *pr; - JSValue val; - int flags; - - if (def_flags & DEFINE_GLOBAL_LEX_VAR) { - p = JS_VALUE_GET_OBJ(ctx->global_var_obj); - flags = JS_PROP_ENUMERABLE | (def_flags & JS_PROP_WRITABLE) | - JS_PROP_CONFIGURABLE; - val = JS_UNINITIALIZED; - } else { - p = JS_VALUE_GET_OBJ(ctx->global_obj); - flags = JS_PROP_ENUMERABLE | JS_PROP_WRITABLE | - (def_flags & JS_PROP_CONFIGURABLE); - val = JS_UNDEFINED; - } - prs = find_own_property1(p, prop); - if (prs) - return 0; - if (!p->extensible) - return 0; - pr = add_property(ctx, p, prop, flags); - if (unlikely(!pr)) - return -1; - pr->u.value = val; - return 0; -} - -/* 'def_flags' is 0 or JS_PROP_CONFIGURABLE. */ -/* XXX: could support exotic global object. */ -static int JS_DefineGlobalFunction(JSContext *ctx, JSAtom prop, - JSValueConst func, int def_flags) -{ - - JSObject *p; - JSShapeProperty *prs; - int flags; - - p = JS_VALUE_GET_OBJ(ctx->global_obj); - prs = find_own_property1(p, prop); - flags = JS_PROP_HAS_VALUE | JS_PROP_THROW; - if (!prs || (prs->flags & JS_PROP_CONFIGURABLE)) { - flags |= JS_PROP_ENUMERABLE | JS_PROP_WRITABLE | def_flags | - JS_PROP_HAS_CONFIGURABLE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE; - } - if (JS_DefineProperty(ctx, ctx->global_obj, prop, func, - JS_UNDEFINED, JS_UNDEFINED, flags) < 0) - return -1; - return 0; -} - -static JSValue JS_GetGlobalVar(JSContext *ctx, JSAtom prop, - BOOL throw_ref_error) -{ - JSObject *p; - JSShapeProperty *prs; - JSProperty *pr; - - /* no exotic behavior is possible in global_var_obj */ - p = JS_VALUE_GET_OBJ(ctx->global_var_obj); - prs = find_own_property(&pr, p, prop); - if (prs) { - /* XXX: should handle JS_PROP_TMASK properties */ - if (unlikely(JS_IsUninitialized(pr->u.value))) - return JS_ThrowReferenceErrorUninitialized(ctx, prs->atom); - return JS_DupValue(ctx, pr->u.value); - } - return JS_GetPropertyInternal(ctx, ctx->global_obj, prop, - ctx->global_obj, throw_ref_error); -} - /* construct a reference to a global variable */ static int JS_GetGlobalVarRef(JSContext *ctx, JSAtom prop, JSValue *sp) { @@ -10026,10 +10356,9 @@ static int JS_GetGlobalVarRef(JSContext *ctx, JSAtom prop, JSValue *sp) p = JS_VALUE_GET_OBJ(ctx->global_var_obj); prs = find_own_property(&pr, p, prop); if (prs) { - /* XXX: should handle JS_PROP_AUTOINIT properties? */ /* XXX: conformance: do these tests in OP_put_var_ref/OP_get_var_ref ? */ - if (unlikely(JS_IsUninitialized(pr->u.value))) { + if (unlikely(JS_IsUninitialized(*pr->u.var_ref->pvalue))) { JS_ThrowReferenceErrorUninitialized(ctx, prs->atom); return -1; } @@ -10052,66 +10381,6 @@ static int JS_GetGlobalVarRef(JSContext *ctx, JSAtom prop, JSValue *sp) return 0; } -/* use for strict variable access: test if the variable exists */ -static int JS_CheckGlobalVar(JSContext *ctx, JSAtom prop) -{ - JSObject *p; - JSShapeProperty *prs; - int ret; - - /* no exotic behavior is possible in global_var_obj */ - p = JS_VALUE_GET_OBJ(ctx->global_var_obj); - prs = find_own_property1(p, prop); - if (prs) { - ret = TRUE; - } else { - ret = JS_HasProperty(ctx, ctx->global_obj, prop); - if (ret < 0) - return -1; - } - return ret; -} - -/* flag = 0: normal variable write - flag = 1: initialize lexical variable - flag = 2: normal variable write, strict check was done before -*/ -static int JS_SetGlobalVar(JSContext *ctx, JSAtom prop, JSValue val, - int flag) -{ - JSObject *p; - JSShapeProperty *prs; - JSProperty *pr; - int flags; - - /* no exotic behavior is possible in global_var_obj */ - p = JS_VALUE_GET_OBJ(ctx->global_var_obj); - prs = find_own_property(&pr, p, prop); - if (prs) { - /* XXX: should handle JS_PROP_AUTOINIT properties? */ - if (flag != 1) { - if (unlikely(JS_IsUninitialized(pr->u.value))) { - JS_FreeValue(ctx, val); - JS_ThrowReferenceErrorUninitialized(ctx, prs->atom); - return -1; - } - if (unlikely(!(prs->flags & JS_PROP_WRITABLE))) { - JS_FreeValue(ctx, val); - return JS_ThrowTypeErrorReadOnly(ctx, JS_PROP_THROW, prop); - } - } - set_value(ctx, &pr->u.value, val); - return 0; - } - /* XXX: add a fast path where the property exists and the object - is not exotic. Otherwise do as in OP_put_ref_value and remove - JS_PROP_NO_ADD which is no longer necessary */ - flags = JS_PROP_THROW_STRICT; - if (is_strict_mode(ctx)) - flags |= JS_PROP_NO_ADD; - return JS_SetPropertyInternal(ctx, ctx->global_obj, prop, val, ctx->global_obj, flags); -} - /* return -1, FALSE or TRUE */ static int JS_DeleteGlobalVar(JSContext *ctx, JSAtom prop) { @@ -10522,6 +10791,15 @@ static inline js_limb_t js_limb_clz(js_limb_t a) } #endif +/* handle a = 0 too */ +static inline js_limb_t js_limb_safe_clz(js_limb_t a) +{ + if (a == 0) + return JS_LIMB_BITS; + else + return js_limb_clz(a); +} + static js_limb_t mp_add(js_limb_t *res, const js_limb_t *op1, const js_limb_t *op2, js_limb_t n, js_limb_t carry) { @@ -11667,6 +11945,7 @@ static JSBigInt *js_bigint_from_string(JSContext *ctx, const char *str, int radix) { const char *p = str; + size_t n_digits1; int is_neg, n_digits, n_limbs, len, log2_radix, n_bits, i; JSBigInt *r; js_limb_t v, c, h; @@ -11678,10 +11957,16 @@ static JSBigInt *js_bigint_from_string(JSContext *ctx, } while (*p == '0') p++; - n_digits = strlen(p); + n_digits1 = strlen(p); + /* the real check for overflox is done js_bigint_new(). Here + we just avoid integer overflow */ + if (n_digits1 > JS_BIGINT_MAX_SIZE * JS_LIMB_BITS) { + JS_ThrowRangeError(ctx, "BigInt is too large to allocate"); + return NULL; + } + n_digits = n_digits1; log2_radix = 32 - clz32(radix - 1); /* ceil(log2(radix)) */ /* compute the maximum number of limbs */ - /* XXX: overflow */ if (radix == 10) { n_bits = (n_digits * 27 + 7) / 8; /* >= ceil(n_digits * log2(10)) */ } else { @@ -11870,7 +12155,7 @@ static JSValue js_bigint_to_string1(JSContext *ctx, JSValueConst val, int radix) r = tmp; } log2_radix = 31 - clz32(radix); /* floor(log2(radix)) */ - n_bits = r->len * JS_LIMB_BITS - js_limb_clz(r->tab[r->len - 1]); + n_bits = r->len * JS_LIMB_BITS - js_limb_safe_clz(r->tab[r->len - 1]); /* n_digits is exact only if radix is a power of two. Otherwise it is >= the exact number of digits */ n_digits = (n_bits + log2_radix - 1) / log2_radix; @@ -11912,11 +12197,10 @@ static JSValue js_bigint_to_string1(JSContext *ctx, JSValueConst val, int radix) bit_pos = i * log2_radix; pos = bit_pos / JS_LIMB_BITS; shift = bit_pos % JS_LIMB_BITS; - if (likely((shift + log2_radix) <= JS_LIMB_BITS)) { - c = r->tab[pos] >> shift; - } else { - c = (r->tab[pos] >> shift) | - (r->tab[pos + 1] << (JS_LIMB_BITS - shift)); + c = r->tab[pos] >> shift; + if ((shift + log2_radix) > JS_LIMB_BITS && + (pos + 1) < r->len) { + c |= r->tab[pos + 1] << (JS_LIMB_BITS - shift); } c &= (radix - 1); *--q = digits[c]; @@ -12117,7 +12401,7 @@ static JSValue js_atof(JSContext *ctx, const char *str, const char **pp, case ATOD_TYPE_FLOAT64: { double d; - d = js_atod(buf,NULL, radix, is_float ? 0 : JS_ATOD_INT_ONLY, + d = js_atod(buf, NULL, radix, is_float ? 0 : JS_ATOD_INT_ONLY, &atod_mem); /* return int or float64 */ val = JS_NewFloat64(ctx, d); @@ -12129,8 +12413,10 @@ static JSValue js_atof(JSContext *ctx, const char *str, const char **pp, if (has_legacy_octal || is_float) goto fail; r = js_bigint_from_string(ctx, buf, radix); - if (!r) - goto mem_error; + if (!r) { + val = JS_EXCEPTION; + goto done; + } val = JS_CompactBigInt(ctx, r); } break; @@ -12889,93 +13175,48 @@ static JSValue JS_ToStringCheckObject(JSContext *ctx, JSValueConst val) return JS_ToString(ctx, val); } -static JSValue JS_ToQuotedString(JSContext *ctx, JSValueConst val1) -{ - JSValue val; - JSString *p; - int i; - uint32_t c; - StringBuffer b_s, *b = &b_s; - char buf[16]; - - val = JS_ToStringCheckObject(ctx, val1); - if (JS_IsException(val)) - return val; - p = JS_VALUE_GET_STRING(val); - - if (string_buffer_init(ctx, b, p->len + 2)) - goto fail; - - if (string_buffer_putc8(b, '\"')) - goto fail; - for(i = 0; i < p->len; ) { - c = string_getc(p, &i); - switch(c) { - case '\t': - c = 't'; - goto quote; - case '\r': - c = 'r'; - goto quote; - case '\n': - c = 'n'; - goto quote; - case '\b': - c = 'b'; - goto quote; - case '\f': - c = 'f'; - goto quote; - case '\"': - case '\\': - quote: - if (string_buffer_putc8(b, '\\')) - goto fail; - if (string_buffer_putc8(b, c)) - goto fail; - break; - default: - if (c < 32 || is_surrogate(c)) { - snprintf(buf, sizeof(buf), "\\u%04x", c); - if (string_buffer_puts8(b, buf)) - goto fail; - } else { - if (string_buffer_putc(b, c)) - goto fail; - } - break; - } - } - if (string_buffer_putc8(b, '\"')) - goto fail; - JS_FreeValue(ctx, val); - return string_buffer_end(b); - fail: - JS_FreeValue(ctx, val); - string_buffer_free(b); - return JS_EXCEPTION; -} - #define JS_PRINT_MAX_DEPTH 8 typedef struct { JSRuntime *rt; JSContext *ctx; /* may be NULL */ JSPrintValueOptions options; - FILE *fo; + JSPrintValueWrite *write_func; + void *write_opaque; int level; JSObject *print_stack[JS_PRINT_MAX_DEPTH]; /* level values */ } JSPrintValueState; static void js_print_value(JSPrintValueState *s, JSValueConst val); +static void js_putc(JSPrintValueState *s, char c) +{ + s->write_func(s->write_opaque, &c, 1); +} + +static void js_puts(JSPrintValueState *s, const char *str) +{ + s->write_func(s->write_opaque, str, strlen(str)); +} + +static void __attribute__((format(printf, 2, 3))) js_printf(JSPrintValueState *s, const char *fmt, ...) +{ + va_list ap; + char buf[256]; + + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + s->write_func(s->write_opaque, buf, strlen(buf)); +} + static void js_print_float64(JSPrintValueState *s, double d) { JSDTOATempMem dtoa_mem; char buf[32]; int len; len = js_dtoa(buf, d, 10, 0, JS_DTOA_FORMAT_FREE | JS_DTOA_MINUS_ZERO, &dtoa_mem); - fwrite(buf, 1, len, s->fo); + s->write_func(s->write_opaque, buf, len); } static uint32_t js_string_get_length(JSValueConst val) @@ -12991,24 +13232,80 @@ static uint32_t js_string_get_length(JSValueConst val) } } +/* pretty print the first 'len' characters of 'p' */ +static void js_print_string1(JSPrintValueState *s, JSString *p, int len, int sep) +{ + uint8_t buf[UTF8_CHAR_LEN_MAX]; + int l, i, c, c1; + + for(i = 0; i < len; i++) { + c = string_get(p, i); + switch(c) { + case '\t': + c = 't'; + goto quote; + case '\r': + c = 'r'; + goto quote; + case '\n': + c = 'n'; + goto quote; + case '\b': + c = 'b'; + goto quote; + case '\f': + c = 'f'; + goto quote; + case '\\': + quote: + js_putc(s, '\\'); + js_putc(s, c); + break; + default: + if (c == sep) + goto quote; + if (c >= 32 && c <= 126) { + js_putc(s, c); + } else if (c < 32 || + (c >= 0x7f && c <= 0x9f)) { + escape: + js_printf(s, "\\u%04x", c); + } else { + if (is_hi_surrogate(c)) { + if ((i + 1) >= len) + goto escape; + c1 = string_get(p, i + 1); + if (!is_lo_surrogate(c1)) + goto escape; + i++; + c = from_surrogate(c, c1); + } else if (is_lo_surrogate(c)) { + goto escape; + } + l = unicode_to_utf8(buf, c); + s->write_func(s->write_opaque, (char *)buf, l); + } + break; + } + } +} + static void js_print_string_rec(JSPrintValueState *s, JSValueConst val, - int sep, uint32_t pos) + int sep, uint32_t pos) { if (JS_VALUE_GET_TAG(val) == JS_TAG_STRING) { JSString *p = JS_VALUE_GET_STRING(val); - uint32_t i, len; + uint32_t len; if (pos < s->options.max_string_length) { len = min_uint32(p->len, s->options.max_string_length - pos); - for(i = 0; i < len; i++) { - JS_DumpChar(s->fo, string_get(p, i), sep); - } + js_print_string1(s, p, len, sep); } } else if (JS_VALUE_GET_TAG(val) == JS_TAG_STRING_ROPE) { JSStringRope *r = JS_VALUE_GET_PTR(val); js_print_string_rec(s, r->left, sep, pos); js_print_string_rec(s, r->right, sep, pos + js_string_get_length(r->left)); } else { - fprintf(s->fo, "", (int)JS_VALUE_GET_TAG(val)); + js_printf(s, "", (int)JS_VALUE_GET_TAG(val)); } } @@ -13017,38 +13314,31 @@ static void js_print_string(JSPrintValueState *s, JSValueConst val) int sep; if (s->options.raw_dump && JS_VALUE_GET_TAG(val) == JS_TAG_STRING) { JSString *p = JS_VALUE_GET_STRING(val); - fprintf(s->fo, "%d", p->header.ref_count); + js_printf(s, "%d", p->header.ref_count); sep = (p->header.ref_count == 1) ? '\"' : '\''; } else { sep = '\"'; } - fputc(sep, s->fo); + js_putc(s, sep); js_print_string_rec(s, val, sep, 0); - fputc(sep, s->fo); + js_putc(s, sep); if (js_string_get_length(val) > s->options.max_string_length) { uint32_t n = js_string_get_length(val) - s->options.max_string_length; - fprintf(s->fo, "... %u more character%s", n, n > 1 ? "s" : ""); + js_printf(s, "... %u more character%s", n, n > 1 ? "s" : ""); } } -static void js_print_raw_string2(JSPrintValueState *s, JSValueConst val, BOOL remove_last_lf) +static void js_print_raw_string(JSPrintValueState *s, JSValueConst val) { const char *cstr; size_t len; cstr = JS_ToCStringLen(s->ctx, &len, val); if (cstr) { - if (remove_last_lf && len > 0 && cstr[len - 1] == '\n') - len--; - fwrite(cstr, 1, len, s->fo); + s->write_func(s->write_opaque, cstr, len); JS_FreeCString(s->ctx, cstr); } } -static void js_print_raw_string(JSPrintValueState *s, JSValueConst val) -{ - js_print_raw_string2(s, val, FALSE); -} - static BOOL is_ascii_ident(const JSString *p) { int i, c; @@ -13064,27 +13354,25 @@ static BOOL is_ascii_ident(const JSString *p) return TRUE; } -static void js_print_atom(JSRuntime *rt, FILE *fo, JSAtom atom) +static void js_print_atom(JSPrintValueState *s, JSAtom atom) { int i; if (__JS_AtomIsTaggedInt(atom)) { - fprintf(fo, "%u", __JS_AtomToUInt32(atom)); + js_printf(s, "%u", __JS_AtomToUInt32(atom)); } else if (atom == JS_ATOM_NULL) { - fprintf(fo, ""); + js_puts(s, ""); } else { - assert(atom < rt->atom_size); + assert(atom < s->rt->atom_size); JSString *p; - p = rt->atom_array[atom]; + p = s->rt->atom_array[atom]; if (is_ascii_ident(p)) { for(i = 0; i < p->len; i++) { - fputc(string_get(p, i), fo); + js_putc(s, string_get(p, i)); } } else { - fputc('"', fo); - for(i = 0; i < p->len; i++) { - JS_DumpChar(fo, string_get(p, i), '\"'); - } - fputc('"', fo); + js_putc(s, '"'); + js_print_string1(s, p, p->len, '\"'); + js_putc(s, '"'); } } } @@ -13118,10 +13406,10 @@ static void js_print_comma(JSPrintValueState *s, int *pcomma_state) case 0: break; case 1: - fprintf(s->fo, ", "); + js_printf(s, ", "); break; case 2: - fprintf(s->fo, " { "); + js_printf(s, " { "); break; } *pcomma_state = 1; @@ -13131,7 +13419,110 @@ static void js_print_more_items(JSPrintValueState *s, int *pcomma_state, uint32_t n) { js_print_comma(s, pcomma_state); - fprintf(s->fo, "... %u more item%s", n, n > 1 ? "s" : ""); + js_printf(s, "... %u more item%s", n, n > 1 ? "s" : ""); +} + +/* similar to js_regexp_toString() but without side effect */ +static void js_print_regexp(JSPrintValueState *s, JSObject *p1) +{ + JSRegExp *re = &p1->u.regexp; + JSString *p; + int i, n, c, c2, bra, flags; + static const char regexp_flags[] = { 'g', 'i', 'm', 's', 'u', 'y', 'd', 'v' }; + + if (!re->pattern || !re->bytecode) { + /* the regexp fields are zeroed at init */ + js_puts(s, "[uninitialized_regexp]"); + return; + } + p = re->pattern; + js_putc(s, '/'); + if (p->len == 0) { + js_puts(s, "(?:)"); + } else { + bra = 0; + for (i = 0, n = p->len; i < n;) { + c2 = -1; + switch (c = string_get(p, i++)) { + case '\\': + if (i < n) + c2 = string_get(p, i++); + break; + case ']': + bra = 0; + break; + case '[': + if (!bra) { + if (i < n && string_get(p, i) == ']') + c2 = string_get(p, i++); + bra = 1; + } + break; + case '\n': + c = '\\'; + c2 = 'n'; + break; + case '\r': + c = '\\'; + c2 = 'r'; + break; + case '/': + if (!bra) { + c = '\\'; + c2 = '/'; + } + break; + } + js_putc(s, c); + if (c2 >= 0) + js_putc(s, c2); + } + } + js_putc(s, '/'); + + flags = lre_get_flags(re->bytecode->u.str8); + for(i = 0; i < countof(regexp_flags); i++) { + if ((flags >> i) & 1) { + js_putc(s, regexp_flags[i]); + } + } +} + +/* similar to js_error_toString() but without side effect */ +static void js_print_error(JSPrintValueState *s, JSObject *p) +{ + const char *str; + size_t len; + + str = get_prop_string(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), JS_ATOM_name); + if (!str) { + js_puts(s, "Error"); + } else { + js_puts(s, str); + JS_FreeCString(s->ctx, str); + } + + str = get_prop_string(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), JS_ATOM_message); + if (str && str[0] != '\0') { + js_puts(s, ": "); + js_puts(s, str); + } + JS_FreeCString(s->ctx, str); + + /* dump the stack if present */ + str = get_prop_string(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), JS_ATOM_stack); + if (str) { + js_putc(s, '\n'); + + /* XXX: should remove the last '\n' in stack as + v8. SpiderMonkey does not do it */ + len = strlen(str); + if (len > 0 && str[len - 1] == '\n') + len--; + s->write_func(s->write_opaque, str, len); + + JS_FreeCString(s->ctx, str); + } } static void js_print_object(JSPrintValueState *s, JSObject *p) @@ -13148,7 +13539,7 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) is_array = FALSE; if (p->class_id == JS_CLASS_ARRAY) { is_array = TRUE; - fprintf(s->fo, "[ "); + js_printf(s, "[ "); /* XXX: print array like properties even if not fast array */ if (p->fast_array) { uint32_t len, n, len1; @@ -13164,7 +13555,7 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) if (p->u.array.count < len) { n = len - p->u.array.count; js_print_comma(s, &comma_state); - fprintf(s->fo, "<%u empty item%s>", n, n > 1 ? "s" : ""); + js_printf(s, "<%u empty item%s>", n, n > 1 ? "s" : ""); } } } else if (p->class_id >= JS_CLASS_UINT8C_ARRAY && p->class_id <= JS_CLASS_FLOAT64_ARRAY) { @@ -13172,8 +13563,8 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) uint32_t len1; int64_t v; - js_print_atom(s->rt, s->fo, rt->class_array[p->class_id].class_name); - fprintf(s->fo, "(%u) [ ", p->u.array.count); + js_print_atom(s, rt->class_array[p->class_id].class_name); + js_printf(s, "(%u) [ ", p->u.array.count); is_array = TRUE; len1 = min_uint32(p->u.array.count, s->options.max_item_count); @@ -13203,10 +13594,13 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) case JS_CLASS_BIG_INT64_ARRAY: v = *(int64_t *)ptr; ta_int64: - fprintf(s->fo, "%" PRId64, v); + js_printf(s, "%" PRId64, v); break; case JS_CLASS_BIG_UINT64_ARRAY: - fprintf(s->fo, "%" PRIu64, *(uint64_t *)ptr); + js_printf(s, "%" PRIu64, *(uint64_t *)ptr); + break; + case JS_CLASS_FLOAT16_ARRAY: + js_print_float64(s, fromfp16(*(uint16_t *)ptr)); break; case JS_CLASS_FLOAT32_ARRAY: js_print_float64(s, *(float *)ptr); @@ -13221,19 +13615,19 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) } else if (p->class_id == JS_CLASS_BYTECODE_FUNCTION || (rt->class_array[p->class_id].call != NULL && p->class_id != JS_CLASS_PROXY)) { - fprintf(s->fo, "[Function"); + js_printf(s, "[Function"); /* XXX: allow dump without ctx */ if (!s->options.raw_dump && s->ctx) { const char *func_name_str; - fputc(' ', s->fo); - func_name_str = get_func_name(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p)); + js_putc(s, ' '); + func_name_str = get_prop_string(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), JS_ATOM_name); if (!func_name_str || func_name_str[0] == '\0') - fputs("(anonymous)", s->fo); + js_puts(s, "(anonymous)"); else - fputs(func_name_str, s->fo); + js_puts(s, func_name_str); JS_FreeCString(s->ctx, func_name_str); } - fprintf(s->fo, "]"); + js_printf(s, "]"); comma_state = 2; } else if (p->class_id == JS_CLASS_MAP || p->class_id == JS_CLASS_SET) { JSMapState *ms = p->u.opaque; @@ -13241,8 +13635,8 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) if (!ms) goto default_obj; - js_print_atom(s->rt, s->fo, rt->class_array[p->class_id].class_name); - fprintf(s->fo, "(%u) { ", ms->record_count); + js_print_atom(s, rt->class_array[p->class_id].class_name); + js_printf(s, "(%u) { ", ms->record_count); i = 0; list_for_each(el, &ms->records) { JSMapRecord *mr = list_entry(el, JSMapRecord, link); @@ -13251,7 +13645,7 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) continue; js_print_value(s, mr->key); if (p->class_id == JS_CLASS_MAP) { - fprintf(s->fo, " => "); + js_printf(s, " => "); js_print_value(s, mr->value); } i++; @@ -13260,43 +13654,27 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) } if (i < ms->record_count) js_print_more_items(s, &comma_state, ms->record_count - i); - } else if (p->class_id == JS_CLASS_REGEXP && s->ctx && !s->options.raw_dump) { - JSValue str = js_regexp_toString(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), 0, NULL); - if (JS_IsException(str)) - goto default_obj; - js_print_raw_string(s, str); - JS_FreeValueRT(s->rt, str); + } else if (p->class_id == JS_CLASS_REGEXP && s->ctx) { + js_print_regexp(s, p); comma_state = 2; - } else if (p->class_id == JS_CLASS_DATE && s->ctx && !s->options.raw_dump) { + } else if (p->class_id == JS_CLASS_DATE && s->ctx) { + /* get_date_string() has no side effect */ JSValue str = get_date_string(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), 0, NULL, 0x23); /* toISOString() */ if (JS_IsException(str)) goto default_obj; js_print_raw_string(s, str); JS_FreeValueRT(s->rt, str); comma_state = 2; - } else if (p->class_id == JS_CLASS_ERROR && s->ctx && !s->options.raw_dump) { - JSValue str = js_error_toString(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), 0, NULL); - if (JS_IsException(str)) - goto default_obj; - js_print_raw_string(s, str); - JS_FreeValueRT(s->rt, str); - /* dump the stack if present */ - str = JS_GetProperty(s->ctx, JS_MKPTR(JS_TAG_OBJECT, p), JS_ATOM_stack); - if (JS_IsString(str)) { - fputc('\n', s->fo); - /* XXX: should remove the last '\n' in stack as - v8. SpiderMonkey does not do it */ - js_print_raw_string2(s, str, TRUE); - } - JS_FreeValueRT(s->rt, str); + } else if (p->class_id == JS_CLASS_ERROR && s->ctx) { + js_print_error(s, p); comma_state = 2; } else { default_obj: if (p->class_id != JS_CLASS_OBJECT) { - js_print_atom(s->rt, s->fo, rt->class_array[p->class_id].class_name); - fprintf(s->fo, " "); + js_print_atom(s, rt->class_array[p->class_id].class_name); + js_printf(s, " "); } - fprintf(s->fo, "{ "); + js_printf(s, "{ "); } sh = p->shape; /* the shape can be NULL while freeing an object */ @@ -13313,39 +13691,39 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) if (j < s->options.max_item_count) { pr = &p->prop[i]; js_print_comma(s, &comma_state); - js_print_atom(s->rt, s->fo, prs->atom); - fprintf(s->fo, ": "); + js_print_atom(s, prs->atom); + js_printf(s, ": "); /* XXX: autoinit property */ if ((prs->flags & JS_PROP_TMASK) == JS_PROP_GETSET) { if (s->options.raw_dump) { - fprintf(s->fo, "[Getter %p Setter %p]", + js_printf(s, "[Getter %p Setter %p]", pr->u.getset.getter, pr->u.getset.setter); } else { if (pr->u.getset.getter && pr->u.getset.setter) { - fprintf(s->fo, "[Getter/Setter]"); + js_printf(s, "[Getter/Setter]"); } else if (pr->u.getset.setter) { - fprintf(s->fo, "[Setter]"); + js_printf(s, "[Setter]"); } else { - fprintf(s->fo, "[Getter]"); + js_printf(s, "[Getter]"); } } } else if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) { if (s->options.raw_dump) { - fprintf(s->fo, "[varref %p]", (void *)pr->u.var_ref); + js_printf(s, "[varref %p]", (void *)pr->u.var_ref); } else { js_print_value(s, *pr->u.var_ref->pvalue); } } else if ((prs->flags & JS_PROP_TMASK) == JS_PROP_AUTOINIT) { if (s->options.raw_dump) { - fprintf(s->fo, "[autoinit %p %d %p]", + js_printf(s, "[autoinit %p %d %p]", (void *)js_autoinit_get_realm(pr), js_autoinit_get_id(pr), (void *)pr->u.init.opaque); } else { /* XXX: could autoinit but need to restart the iteration */ - fprintf(s->fo, "[autoinit]"); + js_printf(s, "[autoinit]"); } } else { js_print_value(s, pr->u.value); @@ -13357,34 +13735,34 @@ static void js_print_object(JSPrintValueState *s, JSObject *p) if (j > s->options.max_item_count) js_print_more_items(s, &comma_state, j - s->options.max_item_count); } - if (s->options.show_closure && js_class_has_bytecode(p->class_id)) { + if (s->options.raw_dump && js_class_has_bytecode(p->class_id)) { JSFunctionBytecode *b = p->u.func.function_bytecode; if (b->closure_var_count) { JSVarRef **var_refs; var_refs = p->u.func.var_refs; js_print_comma(s, &comma_state); - fprintf(s->fo, "[[Closure]]: ["); + js_printf(s, "[[Closure]]: ["); for(i = 0; i < b->closure_var_count; i++) { if (i != 0) - fprintf(s->fo, ", "); + js_printf(s, ", "); js_print_value(s, var_refs[i]->value); } - fprintf(s->fo, " ]"); + js_printf(s, " ]"); } if (p->u.func.home_object) { js_print_comma(s, &comma_state); - fprintf(s->fo, "[[HomeObject]]: "); + js_printf(s, "[[HomeObject]]: "); js_print_value(s, JS_MKPTR(JS_TAG_OBJECT, p->u.func.home_object)); } } if (!is_array) { if (comma_state != 2) { - fprintf(s->fo, " }"); + js_printf(s, " }"); } } else { - fprintf(s->fo, " ]"); + js_printf(s, " ]"); } } @@ -13404,7 +13782,7 @@ static void js_print_value(JSPrintValueState *s, JSValueConst val) switch(tag) { case JS_TAG_INT: - fprintf(s->fo, "%d", JS_VALUE_GET_INT(val)); + js_printf(s, "%d", JS_VALUE_GET_INT(val)); break; case JS_TAG_BOOL: if (JS_VALUE_GET_BOOL(val)) @@ -13424,13 +13802,13 @@ static void js_print_value(JSPrintValueState *s, JSValueConst val) case JS_TAG_UNDEFINED: str = "undefined"; print_str: - fprintf(s->fo, "%s", str); + js_puts(s, str); break; case JS_TAG_FLOAT64: js_print_float64(s, JS_VALUE_GET_FLOAT64(val)); break; case JS_TAG_SHORT_BIG_INT: - fprintf(s->fo, "%" PRId64 "n", (int64_t)JS_VALUE_GET_SHORT_BIG_INT(val)); + js_printf(s, "%" PRId64 "n", (int64_t)JS_VALUE_GET_SHORT_BIG_INT(val)); break; case JS_TAG_BIG_INT: if (!s->options.raw_dump && s->ctx) { @@ -13438,7 +13816,7 @@ static void js_print_value(JSPrintValueState *s, JSValueConst val) if (JS_IsException(str)) goto raw_bigint; js_print_raw_string(s, str); - fputc('n', s->fo); + js_putc(s, 'n'); JS_FreeValueRT(s->rt, str); } else { JSBigInt *p; @@ -13448,27 +13826,27 @@ static void js_print_value(JSPrintValueState *s, JSValueConst val) /* In order to avoid allocations we just dump the limbs */ sgn = js_bigint_sign(p); if (sgn) - fprintf(s->fo, "BigInt.asIntN(%d,", p->len * JS_LIMB_BITS); - fprintf(s->fo, "0x"); + js_printf(s, "BigInt.asIntN(%d,", p->len * JS_LIMB_BITS); + js_printf(s, "0x"); for(i = p->len - 1; i >= 0; i--) { if (i != p->len - 1) - fprintf(s->fo, "_"); + js_putc(s, '_'); #if JS_LIMB_BITS == 32 - fprintf(s->fo, "%08x", p->tab[i]); + js_printf(s, "%08x", p->tab[i]); #else - fprintf(s->fo, "%016" PRIx64, p->tab[i]); + js_printf(s, "%016" PRIx64, p->tab[i]); #endif } - fprintf(s->fo, "n"); + js_putc(s, 'n'); if (sgn) - fprintf(s->fo, ")"); + js_putc(s, ')'); } break; case JS_TAG_STRING: case JS_TAG_STRING_ROPE: if (s->options.raw_dump && tag == JS_TAG_STRING_ROPE) { JSStringRope *r = JS_VALUE_GET_STRING_ROPE(val); - fprintf(s->fo, "[rope len=%d depth=%d]", r->len, r->depth); + js_printf(s, "[rope len=%d depth=%d]", r->len, r->depth); } else { js_print_string(s, val); } @@ -13476,9 +13854,9 @@ static void js_print_value(JSPrintValueState *s, JSValueConst val) case JS_TAG_FUNCTION_BYTECODE: { JSFunctionBytecode *b = JS_VALUE_GET_PTR(val); - fprintf(s->fo, "[bytecode "); - js_print_atom(s->rt, s->fo, b->func_name); - fprintf(s->fo, "]"); + js_puts(s, "[bytecode "); + js_print_atom(s, b->func_name); + js_putc(s, ']'); } break; case JS_TAG_OBJECT: @@ -13487,35 +13865,35 @@ static void js_print_value(JSPrintValueState *s, JSValueConst val) int idx; idx = js_print_stack_index(s, p); if (idx >= 0) { - fprintf(s->fo, "[circular %d]", idx); + js_printf(s, "[circular %d]", idx); } else if (s->level < s->options.max_depth) { s->print_stack[s->level++] = p; js_print_object(s, JS_VALUE_GET_OBJ(val)); s->level--; } else { JSAtom atom = s->rt->class_array[p->class_id].class_name; - fprintf(s->fo, "["); - js_print_atom(s->rt, s->fo, atom); + js_putc(s, '['); + js_print_atom(s, atom); if (s->options.raw_dump) { - fprintf(s->fo, " %p", (void *)p); + js_printf(s, " %p", (void *)p); } - fprintf(s->fo, "]"); + js_putc(s, ']'); } } break; case JS_TAG_SYMBOL: { JSAtomStruct *p = JS_VALUE_GET_PTR(val); - fprintf(s->fo, "Symbol("); - js_print_atom(s->rt, s->fo, js_get_atom_index(s->rt, p)); - fprintf(s->fo, ")"); + js_puts(s, "Symbol("); + js_print_atom(s, js_get_atom_index(s->rt, p)); + js_putc(s, ')'); } break; case JS_TAG_MODULE: - fprintf(s->fo, "[module]"); + js_puts(s, "[module]"); break; default: - fprintf(s->fo, "[unknown tag %d]", tag); + js_printf(s, "[unknown tag %d]", tag); break; } } @@ -13528,8 +13906,9 @@ void JS_PrintValueSetDefaultOptions(JSPrintValueOptions *options) options->max_item_count = 100; } -static void JS_PrintValueInternal(JSRuntime *rt, JSContext *ctx, - FILE *fo, JSValueConst val, const JSPrintValueOptions *options) +static void JS_PrintValueInternal(JSRuntime *rt, JSContext *ctx, + JSPrintValueWrite *write_func, void *write_opaque, + JSValueConst val, const JSPrintValueOptions *options) { JSPrintValueState ss, *s = &ss; if (options) @@ -13546,32 +13925,59 @@ static void JS_PrintValueInternal(JSRuntime *rt, JSContext *ctx, s->options.max_item_count = UINT32_MAX; s->rt = rt; s->ctx = ctx; - s->fo = fo; + s->write_func = write_func; + s->write_opaque = write_opaque; s->level = 0; js_print_value(s, val); } -void JS_PrintValueRT(JSRuntime *rt, FILE *fo, JSValueConst val, const JSPrintValueOptions *options) +void JS_PrintValueRT(JSRuntime *rt, JSPrintValueWrite *write_func, void *write_opaque, + JSValueConst val, const JSPrintValueOptions *options) +{ + JS_PrintValueInternal(rt, NULL, write_func, write_opaque, val, options); +} + +void JS_PrintValue(JSContext *ctx, JSPrintValueWrite *write_func, void *write_opaque, + JSValueConst val, const JSPrintValueOptions *options) +{ + JS_PrintValueInternal(ctx->rt, ctx, write_func, write_opaque, val, options); +} + +static void js_dump_value_write(void *opaque, const char *buf, size_t len) +{ + FILE *fo = opaque; + fwrite(buf, 1, len, fo); +} + +static __maybe_unused void print_atom(JSContext *ctx, JSAtom atom) { - JS_PrintValueInternal(rt, NULL, fo, val, options); + JSPrintValueState ss, *s = &ss; + memset(s, 0, sizeof(*s)); + s->rt = ctx->rt; + s->ctx = ctx; + s->write_func = js_dump_value_write; + s->write_opaque = stdout; + js_print_atom(s, atom); } -void JS_PrintValue(JSContext *ctx, FILE *fo, JSValueConst val, const JSPrintValueOptions *options) +static __maybe_unused void JS_DumpAtom(JSContext *ctx, const char *str, JSAtom atom) { - JS_PrintValueInternal(ctx->rt, ctx, fo, val, options); + printf("%s=", str); + print_atom(ctx, atom); + printf("\n"); } static __maybe_unused void JS_DumpValue(JSContext *ctx, const char *str, JSValueConst val) { printf("%s=", str); - JS_PrintValue(ctx, stdout, val, NULL); + JS_PrintValue(ctx, js_dump_value_write, stdout, val, NULL); printf("\n"); } static __maybe_unused void JS_DumpValueRT(JSRuntime *rt, const char *str, JSValueConst val) { printf("%s=", str); - JS_PrintValueRT(rt, stdout, val, NULL); + JS_PrintValueRT(rt, js_dump_value_write, stdout, val, NULL); printf("\n"); } @@ -13605,7 +14011,7 @@ static __maybe_unused void JS_DumpObject(JSRuntime *rt, JSObject *p) options.max_depth = 1; options.show_hidden = TRUE; options.raw_dump = TRUE; - JS_PrintValueRT(rt, stdout, JS_MKPTR(JS_TAG_OBJECT, p), &options); + JS_PrintValueRT(rt, js_dump_value_write, stdout, JS_MKPTR(JS_TAG_OBJECT, p), &options); printf("\n"); } @@ -13634,6 +14040,9 @@ static __maybe_unused void JS_DumpGCObject(JSRuntime *rt, JSGCObjectHeader *p) case JS_GC_OBJ_TYPE_JS_CONTEXT: printf("[js_context]"); break; + case JS_GC_OBJ_TYPE_MODULE: + printf("[module]"); + break; default: printf("[unknown %d]", p->gc_obj_type); break; @@ -14884,8 +15293,8 @@ static BOOL js_strict_eq2(JSContext *ctx, JSValue op1, JSValue op2, if (!tag_is_string(tag2)) { res = FALSE; } else if (tag1 == JS_TAG_STRING && tag2 == JS_TAG_STRING) { - res = (js_string_compare(ctx, JS_VALUE_GET_STRING(op1), - JS_VALUE_GET_STRING(op2)) == 0); + res = js_string_eq(ctx, JS_VALUE_GET_STRING(op1), + JS_VALUE_GET_STRING(op2)); } else { res = (js_string_rope_compare(ctx, op1, op2, TRUE) == 0); } @@ -15363,26 +15772,6 @@ static JSValue js_build_mapped_arguments(JSContext *ctx, int argc, return JS_EXCEPTION; } -static JSValue js_build_rest(JSContext *ctx, int first, int argc, JSValueConst *argv) -{ - JSValue val; - int i, ret; - - val = JS_NewArray(ctx); - if (JS_IsException(val)) - return val; - for (i = first; i < argc; i++) { - ret = JS_DefinePropertyValueUint32(ctx, val, i - first, - JS_DupValue(ctx, argv[i]), - JS_PROP_C_W_E); - if (ret < 0) { - JS_FreeValue(ctx, val); - return JS_EXCEPTION; - } - } - return val; -} - static JSValue build_for_in_iterator(JSContext *ctx, JSValue obj) { JSObject *p, *p1; @@ -15482,7 +15871,7 @@ static __exception int js_for_in_prepare_prototype_chain_enum(JSContext *ctx, JS_FreeValue(ctx, obj1); goto fail; } - js_free_prop_enum(ctx, tab_atom, tab_atom_count); + JS_FreePropertyEnum(ctx, tab_atom, tab_atom_count); if (tab_atom_count != 0) { JS_FreeValue(ctx, obj1); goto slow_path; @@ -15566,7 +15955,7 @@ static __exception int js_for_in_next(JSContext *ctx, JSValue *sp) JS_GPN_STRING_MASK | JS_GPN_SET_ENUM)) { return -1; } - js_free_prop_enum(ctx, it->tab_atom, it->atom_count); + JS_FreePropertyEnum(ctx, it->tab_atom, it->atom_count); it->tab_atom = tab_atom; it->atom_count = tab_atom_count; it->idx = 0; @@ -15947,6 +16336,7 @@ static __exception int js_append_enumerate(JSContext *ctx, JSValue *sp) int is_array_iterator; JSValue *arrp; uint32_t i, count32, pos; + JSCFunctionType ft; if (JS_VALUE_GET_TAG(sp[-2]) != JS_TAG_INT) { JS_ThrowInternalError(ctx, "invalid index for append"); @@ -15964,8 +16354,8 @@ static __exception int js_append_enumerate(JSContext *ctx, JSValue *sp) iterator = JS_GetProperty(ctx, sp[-1], JS_ATOM_Symbol_iterator); if (JS_IsException(iterator)) return -1; - is_array_iterator = JS_IsCFunction(ctx, iterator, - (JSCFunction *)js_create_array_iterator, + ft.generic_magic = js_create_array_iterator; + is_array_iterator = JS_IsCFunction(ctx, iterator, ft.generic, JS_ITERATOR_KIND_VALUE); JS_FreeValue(ctx, iterator); @@ -15977,8 +16367,10 @@ static __exception int js_append_enumerate(JSContext *ctx, JSValue *sp) JS_FreeValue(ctx, enumobj); return -1; } + + ft.iterator_next = js_array_iterator_next; if (is_array_iterator - && JS_IsCFunction(ctx, method, (JSCFunction *)js_array_iterator_next, 0) + && JS_IsCFunction(ctx, method, ft.generic, 0) && js_get_fast_array(ctx, sp[-1], &arrp, &count32)) { uint32_t len; if (js_get_length32(ctx, &len, sp[-1])) @@ -16089,10 +16481,10 @@ static __exception int JS_CopyDataProperties(JSContext *ctx, if (ret < 0) goto exception; } - js_free_prop_enum(ctx, tab_atom, tab_atom_count); + JS_FreePropertyEnum(ctx, tab_atom, tab_atom_count); return 0; exception: - js_free_prop_enum(ctx, tab_atom, tab_atom_count); + JS_FreePropertyEnum(ctx, tab_atom, tab_atom_count); return -1; } @@ -16102,6 +16494,25 @@ static JSValueConst JS_GetActiveFunction(JSContext *ctx) return ctx->rt->current_stack_frame->cur_func; } +static JSVarRef *js_create_var_ref(JSContext *ctx, BOOL is_lexical) +{ + JSVarRef *var_ref; + var_ref = js_malloc(ctx, sizeof(JSVarRef)); + if (!var_ref) + return NULL; + var_ref->header.ref_count = 1; + if (is_lexical) + var_ref->value = JS_UNINITIALIZED; + else + var_ref->value = JS_UNDEFINED; + var_ref->pvalue = &var_ref->value; + var_ref->is_detached = TRUE; + var_ref->is_lexical = FALSE; + var_ref->is_const = FALSE; + add_gc_object(ctx->rt, &var_ref->header, JS_GC_OBJ_TYPE_VAR_REF); + return var_ref; +} + static JSVarRef *get_var_ref(JSContext *ctx, JSStackFrame *sf, int var_idx, BOOL is_arg) { @@ -16128,6 +16539,8 @@ static JSVarRef *get_var_ref(JSContext *ctx, JSStackFrame *sf, var_ref->header.ref_count = 1; add_gc_object(ctx->rt, &var_ref->header, JS_GC_OBJ_TYPE_VAR_REF); var_ref->is_detached = FALSE; + var_ref->is_lexical = FALSE; + var_ref->is_const = FALSE; list_add_tail(&var_ref->var_ref_link, &sf->var_ref_list); if (sf->js_mode & JS_MODE_ASYNC) { /* The stack frame is detached and may be destroyed at any @@ -16147,10 +16560,217 @@ static JSVarRef *get_var_ref(JSContext *ctx, JSStackFrame *sf, return var_ref; } +static void js_global_object_finalizer(JSRuntime *rt, JSValue obj) +{ + JSObject *p = JS_VALUE_GET_OBJ(obj); + JS_FreeValueRT(rt, p->u.global_object.uninitialized_vars); +} + +static void js_global_object_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func) +{ + JSObject *p = JS_VALUE_GET_OBJ(val); + JS_MarkValue(rt, p->u.global_object.uninitialized_vars, mark_func); +} + +static JSVarRef *js_global_object_get_uninitialized_var(JSContext *ctx, JSObject *p1, + JSAtom atom) +{ + JSObject *p = JS_VALUE_GET_OBJ(p1->u.global_object.uninitialized_vars); + JSShapeProperty *prs; + JSProperty *pr; + JSVarRef *var_ref; + + prs = find_own_property(&pr, p, atom); + if (prs) { + assert((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF); + var_ref = pr->u.var_ref; + var_ref->header.ref_count++; + return var_ref; + } + + var_ref = js_create_var_ref(ctx, TRUE); + if (!var_ref) + return NULL; + pr = add_property(ctx, p, atom, JS_PROP_C_W_E | JS_PROP_VARREF); + if (unlikely(!pr)) { + free_var_ref(ctx->rt, var_ref); + return NULL; + } + pr->u.var_ref = var_ref; + var_ref->header.ref_count++; + return var_ref; +} + +/* return a new variable reference. Get it from the uninitialized + variables if it is present. Return NULL in case of memory error. */ +static JSVarRef *js_global_object_find_uninitialized_var(JSContext *ctx, JSObject *p, + JSAtom atom, BOOL is_lexical) +{ + JSObject *p1; + JSShapeProperty *prs; + JSProperty *pr; + JSVarRef *var_ref; + + p1 = JS_VALUE_GET_OBJ(p->u.global_object.uninitialized_vars); + prs = find_own_property(&pr, p1, atom); + if (prs) { + assert((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF); + var_ref = pr->u.var_ref; + var_ref->header.ref_count++; + delete_property(ctx, p1, atom); + if (!is_lexical) + var_ref->value = JS_UNDEFINED; + } else { + var_ref = js_create_var_ref(ctx, is_lexical); + if (!var_ref) + return NULL; + } + return var_ref; +} + +static JSVarRef *js_closure_define_global_var(JSContext *ctx, JSClosureVar *cv, + BOOL is_direct_or_indirect_eval) +{ + JSObject *p, *p1; + JSShapeProperty *prs; + int flags; + JSProperty *pr; + JSVarRef *var_ref; + + if (cv->is_lexical) { + p = JS_VALUE_GET_OBJ(ctx->global_var_obj); + flags = JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE; + if (!cv->is_const) + flags |= JS_PROP_WRITABLE; + + prs = find_own_property(&pr, p, cv->var_name); + if (prs) { + assert((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF); + var_ref = pr->u.var_ref; + var_ref->header.ref_count++; + return var_ref; + } + + /* if there is a corresponding global variable, reuse its + reference and create a new one for the global variable */ + p1 = JS_VALUE_GET_OBJ(ctx->global_obj); + prs = find_own_property(&pr, p1, cv->var_name); + if (prs && (prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) { + JSVarRef *var_ref1; + var_ref1 = js_create_var_ref(ctx, FALSE); + if (!var_ref1) + return NULL; + var_ref = pr->u.var_ref; + var_ref1->value = var_ref->value; + var_ref->value = JS_UNINITIALIZED; + pr->u.var_ref = var_ref1; + goto add_var_ref; + } + } else { + p = JS_VALUE_GET_OBJ(ctx->global_obj); + flags = JS_PROP_ENUMERABLE | JS_PROP_WRITABLE; + if (is_direct_or_indirect_eval) + flags |= JS_PROP_CONFIGURABLE; + + retry: + prs = find_own_property(&pr, p, cv->var_name); + if (prs) { + if (unlikely((prs->flags & JS_PROP_TMASK) == JS_PROP_AUTOINIT)) { + if (JS_AutoInitProperty(ctx, p, cv->var_name, pr, prs)) + return NULL; + goto retry; + } else if ((prs->flags & JS_PROP_TMASK) != JS_PROP_VARREF) { + var_ref = js_global_object_get_uninitialized_var(ctx, p, cv->var_name); + if (!var_ref) + return NULL; + } else { + var_ref = pr->u.var_ref; + var_ref->header.ref_count++; + } + if (cv->var_kind == JS_VAR_GLOBAL_FUNCTION_DECL && + (prs->flags & JS_PROP_CONFIGURABLE)) { + /* update the property flags if possible when + declaring a global function */ + if ((prs->flags & JS_PROP_TMASK) == JS_PROP_GETSET) { + free_property(ctx->rt, pr, prs->flags); + prs->flags = flags | JS_PROP_VARREF; + pr->u.var_ref = var_ref; + var_ref->header.ref_count++; + } else { + assert((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF); + prs->flags = (prs->flags & ~JS_PROP_C_W_E) | flags; + } + var_ref->is_const = FALSE; + } + return var_ref; + } + + if (!p->extensible) { + return js_global_object_get_uninitialized_var(ctx, p, cv->var_name); + } + } + + /* if there is a corresponding uninitialized variable, use it */ + p1 = JS_VALUE_GET_OBJ(ctx->global_obj); + var_ref = js_global_object_find_uninitialized_var(ctx, p1, cv->var_name, cv->is_lexical); + if (!var_ref) + return NULL; + add_var_ref: + if (cv->is_lexical) { + var_ref->is_lexical = TRUE; + var_ref->is_const = cv->is_const; + } + + pr = add_property(ctx, p, cv->var_name, flags | JS_PROP_VARREF); + if (unlikely(!pr)) { + free_var_ref(ctx->rt, var_ref); + return NULL; + } + pr->u.var_ref = var_ref; + var_ref->header.ref_count++; + return var_ref; +} + +static JSVarRef *js_closure_global_var(JSContext *ctx, JSClosureVar *cv) +{ + JSObject *p; + JSShapeProperty *prs; + JSProperty *pr; + JSVarRef *var_ref; + + p = JS_VALUE_GET_OBJ(ctx->global_var_obj); + prs = find_own_property(&pr, p, cv->var_name); + if (prs) { + assert((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF); + var_ref = pr->u.var_ref; + var_ref->header.ref_count++; + return var_ref; + } + p = JS_VALUE_GET_OBJ(ctx->global_obj); + redo: + prs = find_own_property(&pr, p, cv->var_name); + if (prs) { + if (unlikely((prs->flags & JS_PROP_TMASK) == JS_PROP_AUTOINIT)) { + /* Instantiate property and retry */ + if (JS_AutoInitProperty(ctx, p, cv->var_name, pr, prs)) + return NULL; + goto redo; + } + if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) { + var_ref = pr->u.var_ref; + var_ref->header.ref_count++; + return var_ref; + } + } + return js_global_object_get_uninitialized_var(ctx, p, cv->var_name); +} + static JSValue js_closure2(JSContext *ctx, JSValue func_obj, JSFunctionBytecode *b, JSVarRef **cur_var_refs, - JSStackFrame *sf) + JSStackFrame *sf, + BOOL is_eval, JSModuleDef *m) { JSObject *p; JSVarRef **var_refs; @@ -16165,18 +16785,56 @@ static JSValue js_closure2(JSContext *ctx, JSValue func_obj, if (!var_refs) goto fail; p->u.func.var_refs = var_refs; + if (is_eval) { + /* first pass to check the global variable definitions */ + for(i = 0; i < b->closure_var_count; i++) { + JSClosureVar *cv = &b->closure_var[i]; + if (cv->closure_type == JS_CLOSURE_GLOBAL_DECL) { + int flags; + flags = 0; + if (cv->is_lexical) + flags |= DEFINE_GLOBAL_LEX_VAR; + if (cv->var_kind == JS_VAR_GLOBAL_FUNCTION_DECL) + flags |= DEFINE_GLOBAL_FUNC_VAR; + if (JS_CheckDefineGlobalVar(ctx, cv->var_name, flags)) + goto fail; + } + } + } for(i = 0; i < b->closure_var_count; i++) { JSClosureVar *cv = &b->closure_var[i]; JSVarRef *var_ref; - if (cv->is_local) { + switch(cv->closure_type) { + case JS_CLOSURE_MODULE_IMPORT: + /* imported from other modules */ + continue; + case JS_CLOSURE_MODULE_DECL: + var_ref = js_create_var_ref(ctx, cv->is_lexical); + break; + case JS_CLOSURE_GLOBAL_DECL: + var_ref = js_closure_define_global_var(ctx, cv, b->is_direct_or_indirect_eval); + break; + case JS_CLOSURE_GLOBAL: + var_ref = js_closure_global_var(ctx, cv); + break; + case JS_CLOSURE_LOCAL: /* reuse the existing variable reference if it already exists */ - var_ref = get_var_ref(ctx, sf, cv->var_idx, cv->is_arg); - if (!var_ref) - goto fail; - } else { + var_ref = get_var_ref(ctx, sf, cv->var_idx, FALSE); + break; + case JS_CLOSURE_ARG: + /* reuse the existing variable reference if it already exists */ + var_ref = get_var_ref(ctx, sf, cv->var_idx, TRUE); + break; + case JS_CLOSURE_REF: + case JS_CLOSURE_GLOBAL_REF: var_ref = cur_var_refs[cv->var_idx]; var_ref->header.ref_count++; + break; + default: + abort(); } + if (!var_ref) + goto fail; var_refs[i] = var_ref; } } @@ -16217,7 +16875,7 @@ static const uint16_t func_kind_to_class_id[] = { static JSValue js_closure(JSContext *ctx, JSValue bfunc, JSVarRef **cur_var_refs, - JSStackFrame *sf) + JSStackFrame *sf, BOOL is_eval) { JSFunctionBytecode *b; JSValue func_obj; @@ -16229,7 +16887,7 @@ static JSValue js_closure(JSContext *ctx, JSValue bfunc, JS_FreeValue(ctx, bfunc); return JS_EXCEPTION; } - func_obj = js_closure2(ctx, func_obj, b, cur_var_refs, sf); + func_obj = js_closure2(ctx, func_obj, b, cur_var_refs, sf, is_eval, NULL); if (JS_IsException(func_obj)) { /* bfunc has been freed */ goto fail; @@ -16316,7 +16974,7 @@ static int js_op_define_class(JSContext *ctx, JSValue *sp, JS_CLASS_BYTECODE_FUNCTION); if (JS_IsException(ctor)) goto fail; - ctor = js_closure2(ctx, ctor, b, cur_var_refs, sf); + ctor = js_closure2(ctx, ctor, b, cur_var_refs, sf, FALSE, NULL); bfunc = JS_UNDEFINED; if (JS_IsException(ctor)) goto fail; @@ -16749,25 +17407,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, *sp++ = JS_DupValue(ctx, b->cpool[*pc++]); BREAK; CASE(OP_fclosure8): - *sp++ = js_closure(ctx, JS_DupValue(ctx, b->cpool[*pc++]), var_refs, sf); + *sp++ = js_closure(ctx, JS_DupValue(ctx, b->cpool[*pc++]), var_refs, sf, FALSE); if (unlikely(JS_IsException(sp[-1]))) goto exception; BREAK; CASE(OP_push_empty_string): *sp++ = JS_AtomToString(ctx, JS_ATOM_empty_string); BREAK; - CASE(OP_get_length): - { - JSValue val; - - sf->cur_pc = pc; - val = JS_GetProperty(ctx, sp[-1], JS_ATOM_length); - if (unlikely(JS_IsException(val))) - goto exception; - JS_FreeValue(ctx, sp[-1]); - sp[-1] = val; - } - BREAK; #endif CASE(OP_push_atom_value): *sp++ = JS_AtomToValue(ctx, get_u32(pc)); @@ -16862,7 +17508,8 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, { int first = get_u16(pc); pc += 2; - *sp++ = js_build_rest(ctx, first, argc, (JSValueConst *)argv); + first = min_int(first, argc); + *sp++ = js_create_array(ctx, argc - first, (JSValueConst *)(argv + first)); if (unlikely(JS_IsException(sp[-1]))) goto exception; } @@ -17014,7 +17661,7 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, { JSValue bfunc = JS_DupValue(ctx, b->cpool[get_u32(pc)]); pc += 4; - *sp++ = js_closure(ctx, bfunc, var_refs, sf); + *sp++ = js_closure(ctx, bfunc, var_refs, sf, FALSE); if (unlikely(JS_IsException(sp[-1]))) goto exception; } @@ -17085,27 +17732,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, } BREAK; CASE(OP_array_from): - { - int i, ret; - - call_argc = get_u16(pc); - pc += 2; - ret_val = JS_NewArray(ctx); - if (unlikely(JS_IsException(ret_val))) - goto exception; - call_argv = sp - call_argc; - for(i = 0; i < call_argc; i++) { - ret = JS_DefinePropertyValue(ctx, ret_val, __JS_AtomFromUInt32(i), call_argv[i], - JS_PROP_C_W_E | JS_PROP_THROW); - call_argv[i] = JS_UNDEFINED; - if (ret < 0) { - JS_FreeValue(ctx, ret_val); - goto exception; - } - } - sp -= call_argc; - *sp++ = ret_val; - } + call_argc = get_u16(pc); + pc += 2; + ret_val = js_create_array_free(ctx, call_argc, sp - call_argc); + sp -= call_argc; + if (unlikely(JS_IsException(ret_val))) + goto exception; + *sp++ = ret_val; BREAK; CASE(OP_apply): @@ -17287,8 +17920,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, CASE(OP_regexp): { - sp[-2] = js_regexp_constructor_internal(ctx, JS_UNDEFINED, - sp[-2], sp[-1]); + JSValue obj; + obj = JS_NewObjectClass(ctx, JS_CLASS_REGEXP); + if (JS_IsException(obj)) + goto exception; + sp[-2] = js_regexp_set_internal(ctx, obj, sp[-2], sp[-1]); + if (JS_IsException(sp[-2])) + goto exception; sp--; } BREAK; @@ -17309,120 +17947,86 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, { JSValue val; sf->cur_pc = pc; - val = js_dynamic_import(ctx, sp[-1]); + val = js_dynamic_import(ctx, sp[-2], sp[-1]); if (JS_IsException(val)) goto exception; + JS_FreeValue(ctx, sp[-2]); JS_FreeValue(ctx, sp[-1]); + sp--; sp[-1] = val; } BREAK; - CASE(OP_check_var): - { - int ret; - JSAtom atom; - atom = get_u32(pc); - pc += 4; - sf->cur_pc = pc; - - ret = JS_CheckGlobalVar(ctx, atom); - if (ret < 0) - goto exception; - *sp++ = JS_NewBool(ctx, ret); - } - BREAK; - CASE(OP_get_var_undef): CASE(OP_get_var): { + int idx; JSValue val; - JSAtom atom; - atom = get_u32(pc); - pc += 4; - sf->cur_pc = pc; - - val = JS_GetGlobalVar(ctx, atom, opcode - OP_get_var_undef); - if (unlikely(JS_IsException(val))) - goto exception; - *sp++ = val; + idx = get_u16(pc); + pc += 2; + val = *var_refs[idx]->pvalue; + if (unlikely(JS_IsUninitialized(val))) { + JSClosureVar *cv = &b->closure_var[idx]; + if (cv->is_lexical) { + JS_ThrowReferenceErrorUninitialized(ctx, cv->var_name); + goto exception; + } else { + sf->cur_pc = pc; + sp[0] = JS_GetPropertyInternal(ctx, ctx->global_obj, + cv->var_name, + ctx->global_obj, + opcode - OP_get_var_undef); + if (JS_IsException(sp[0])) + goto exception; + } + } else { + sp[0] = JS_DupValue(ctx, val); + } + sp++; } BREAK; CASE(OP_put_var): CASE(OP_put_var_init): { - int ret; - JSAtom atom; - atom = get_u32(pc); - pc += 4; - sf->cur_pc = pc; - - ret = JS_SetGlobalVar(ctx, atom, sp[-1], opcode - OP_put_var); - sp--; - if (unlikely(ret < 0)) - goto exception; - } - BREAK; - - CASE(OP_put_var_strict): - { - int ret; - JSAtom atom; - atom = get_u32(pc); - pc += 4; - sf->cur_pc = pc; - - /* sp[-2] is JS_TRUE or JS_FALSE */ - if (unlikely(!JS_VALUE_GET_INT(sp[-2]))) { - JS_ThrowReferenceErrorNotDefined(ctx, atom); - goto exception; + int idx, ret; + JSVarRef *var_ref; + idx = get_u16(pc); + pc += 2; + var_ref = var_refs[idx]; + if (unlikely(JS_IsUninitialized(*var_ref->pvalue) || + var_ref->is_const)) { + JSClosureVar *cv = &b->closure_var[idx]; + if (var_ref->is_lexical) { + if (opcode == OP_put_var_init) + goto put_var_ok; + if (JS_IsUninitialized(*var_ref->pvalue)) + JS_ThrowReferenceErrorUninitialized(ctx, cv->var_name); + else + JS_ThrowTypeErrorReadOnly(ctx, JS_PROP_THROW, cv->var_name); + goto exception; + } else { + sf->cur_pc = pc; + ret = JS_HasProperty(ctx, ctx->global_obj, cv->var_name); + if (ret < 0) + goto exception; + if (ret == 0 && is_strict_mode(ctx)) { + JS_ThrowReferenceErrorNotDefined(ctx, cv->var_name); + goto exception; + } + ret = JS_SetPropertyInternal(ctx, ctx->global_obj, cv->var_name, sp[-1], + ctx->global_obj, JS_PROP_THROW_STRICT); + sp--; + if (ret < 0) + goto exception; + } + } else { + put_var_ok: + set_value(ctx, var_ref->pvalue, sp[-1]); + sp--; } - ret = JS_SetGlobalVar(ctx, atom, sp[-1], 2); - sp -= 2; - if (unlikely(ret < 0)) - goto exception; - } - BREAK; - - CASE(OP_check_define_var): - { - JSAtom atom; - int flags; - atom = get_u32(pc); - flags = pc[4]; - pc += 5; - sf->cur_pc = pc; - if (JS_CheckDefineGlobalVar(ctx, atom, flags)) - goto exception; } BREAK; - CASE(OP_define_var): - { - JSAtom atom; - int flags; - atom = get_u32(pc); - flags = pc[4]; - pc += 5; - sf->cur_pc = pc; - if (JS_DefineGlobalVar(ctx, atom, flags)) - goto exception; - } - BREAK; - CASE(OP_define_func): - { - JSAtom atom; - int flags; - atom = get_u32(pc); - flags = pc[4]; - pc += 5; - sf->cur_pc = pc; - if (JS_DefineGlobalFunction(ctx, atom, sp[-1], flags)) - goto exception; - JS_FreeValue(ctx, sp[-1]); - sp--; - } - BREAK; - CASE(OP_get_loc): { int idx; @@ -17987,51 +18591,114 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, } BREAK; - CASE(OP_get_field): - { - JSValue val; - JSAtom atom; - atom = get_u32(pc); - pc += 4; - - sf->cur_pc = pc; - val = JS_GetProperty(ctx, sp[-1], atom); - if (unlikely(JS_IsException(val))) - goto exception; - JS_FreeValue(ctx, sp[-1]); - sp[-1] = val; +#define GET_FIELD_INLINE(name, keep, is_length) \ + { \ + JSValue val, obj; \ + JSAtom atom; \ + JSObject *p; \ + JSProperty *pr; \ + JSShapeProperty *prs; \ + \ + if (is_length) { \ + atom = JS_ATOM_length; \ + } else { \ + atom = get_u32(pc); \ + pc += 4; \ + } \ + \ + obj = sp[-1]; \ + if (likely(JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT)) { \ + p = JS_VALUE_GET_OBJ(obj); \ + for(;;) { \ + prs = find_own_property(&pr, p, atom); \ + if (prs) { \ + /* found */ \ + if (unlikely(prs->flags & JS_PROP_TMASK)) \ + goto name ## _slow_path; \ + val = JS_DupValue(ctx, pr->u.value); \ + break; \ + } \ + if (unlikely(p->is_exotic)) { \ + /* XXX: should avoid the slow path for arrays \ + and typed arrays by ensuring that 'prop' is \ + not numeric */ \ + obj = JS_MKPTR(JS_TAG_OBJECT, p); \ + goto name ## _slow_path; \ + } \ + p = p->shape->proto; \ + if (!p) { \ + val = JS_UNDEFINED; \ + break; \ + } \ + } \ + } else { \ + name ## _slow_path: \ + sf->cur_pc = pc; \ + val = JS_GetPropertyInternal(ctx, obj, atom, sp[-1], 0); \ + if (unlikely(JS_IsException(val))) \ + goto exception; \ + } \ + if (keep) { \ + *sp++ = val; \ + } else { \ + JS_FreeValue(ctx, sp[-1]); \ + sp[-1] = val; \ + } \ } + + + CASE(OP_get_field): + GET_FIELD_INLINE(get_field, 0, 0); BREAK; CASE(OP_get_field2): - { - JSValue val; - JSAtom atom; - atom = get_u32(pc); - pc += 4; - - sf->cur_pc = pc; - val = JS_GetProperty(ctx, sp[-1], atom); - if (unlikely(JS_IsException(val))) - goto exception; - *sp++ = val; - } + GET_FIELD_INLINE(get_field2, 1, 0); BREAK; +#if SHORT_OPCODES + CASE(OP_get_length): + GET_FIELD_INLINE(get_length, 0, 1); + BREAK; +#endif + CASE(OP_put_field): { int ret; + JSValue obj; JSAtom atom; + JSObject *p; + JSProperty *pr; + JSShapeProperty *prs; + atom = get_u32(pc); pc += 4; - sf->cur_pc = pc; - ret = JS_SetPropertyInternal(ctx, sp[-2], atom, sp[-1], sp[-2], - JS_PROP_THROW_STRICT); - JS_FreeValue(ctx, sp[-2]); - sp -= 2; - if (unlikely(ret < 0)) - goto exception; + obj = sp[-2]; + if (likely(JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT)) { + p = JS_VALUE_GET_OBJ(obj); + prs = find_own_property(&pr, p, atom); + if (!prs) + goto put_field_slow_path; + if (likely((prs->flags & (JS_PROP_TMASK | JS_PROP_WRITABLE | + JS_PROP_LENGTH)) == JS_PROP_WRITABLE)) { + /* fast path */ + set_value(ctx, &pr->u.value, sp[-1]); + } else { + goto put_field_slow_path; + } + JS_FreeValue(ctx, obj); + sp -= 2; + } else { + put_field_slow_path: + sf->cur_pc = pc; + ret = JS_SetPropertyInternal(ctx, obj, atom, sp[-1], obj, + JS_PROP_THROW_STRICT); + JS_FreeValue(ctx, obj); + sp -= 2; + if (unlikely(ret < 0)) + goto exception; + } + } BREAK; @@ -18213,61 +18880,95 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, } BREAK; - CASE(OP_get_array_el): - { - JSValue val; - - sf->cur_pc = pc; - val = JS_GetPropertyValue(ctx, sp[-2], sp[-1]); - JS_FreeValue(ctx, sp[-2]); - sp[-2] = val; - sp--; - if (unlikely(JS_IsException(val))) - goto exception; +#define GET_ARRAY_EL_INLINE(name, keep) \ + { \ + JSValue val, obj, prop; \ + JSObject *p; \ + uint32_t idx; \ + \ + obj = sp[-2]; \ + prop = sp[-1]; \ + if (likely(JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT && \ + JS_VALUE_GET_TAG(prop) == JS_TAG_INT)) { \ + p = JS_VALUE_GET_OBJ(obj); \ + idx = JS_VALUE_GET_INT(prop); \ + if (unlikely(p->class_id != JS_CLASS_ARRAY)) \ + goto name ## _slow_path; \ + if (unlikely(idx >= p->u.array.count)) \ + goto name ## _slow_path; \ + val = JS_DupValue(ctx, p->u.array.u.values[idx]); \ + } else { \ + name ## _slow_path: \ + sf->cur_pc = pc; \ + val = JS_GetPropertyValue(ctx, obj, prop); \ + if (unlikely(JS_IsException(val))) { \ + if (keep) \ + sp[-1] = JS_UNDEFINED; \ + else \ + sp--; \ + goto exception; \ + } \ + } \ + if (keep) { \ + sp[-1] = val; \ + } else { \ + JS_FreeValue(ctx, obj); \ + sp[-2] = val; \ + sp--; \ + } \ } + + CASE(OP_get_array_el): + GET_ARRAY_EL_INLINE(get_array_el, 0); BREAK; CASE(OP_get_array_el2): - { - JSValue val; - - sf->cur_pc = pc; - val = JS_GetPropertyValue(ctx, sp[-2], sp[-1]); - sp[-1] = val; - if (unlikely(JS_IsException(val))) - goto exception; - } + GET_ARRAY_EL_INLINE(get_array_el2, 1); BREAK; CASE(OP_get_array_el3): { JSValue val; + JSObject *p; + uint32_t idx; - switch (JS_VALUE_GET_TAG(sp[-2])) { - case JS_TAG_INT: - case JS_TAG_STRING: - case JS_TAG_SYMBOL: - /* undefined and null are tested in JS_GetPropertyValue() */ - break; - default: - /* must be tested nefore JS_ToPropertyKey */ - if (unlikely(JS_IsUndefined(sp[-2]) || JS_IsNull(sp[-2]))) { - JS_ThrowTypeError(ctx, "value has no property"); - goto exception; + if (likely(JS_VALUE_GET_TAG(sp[-2]) == JS_TAG_OBJECT && + JS_VALUE_GET_TAG(sp[-1]) == JS_TAG_INT)) { + p = JS_VALUE_GET_OBJ(sp[-2]); + idx = JS_VALUE_GET_INT(sp[-1]); + if (unlikely(p->class_id != JS_CLASS_ARRAY)) + goto get_array_el3_slow_path; + if (unlikely(idx >= p->u.array.count)) + goto get_array_el3_slow_path; + val = JS_DupValue(ctx, p->u.array.u.values[idx]); + } else { + get_array_el3_slow_path: + switch (JS_VALUE_GET_TAG(sp[-1])) { + case JS_TAG_INT: + case JS_TAG_STRING: + case JS_TAG_SYMBOL: + /* undefined and null are tested in JS_GetPropertyValue() */ + break; + default: + /* must be tested before JS_ToPropertyKey */ + if (unlikely(JS_IsUndefined(sp[-2]) || JS_IsNull(sp[-2]))) { + JS_ThrowTypeError(ctx, "value has no property"); + goto exception; + } + sf->cur_pc = pc; + ret_val = JS_ToPropertyKey(ctx, sp[-1]); + if (JS_IsException(ret_val)) + goto exception; + JS_FreeValue(ctx, sp[-1]); + sp[-1] = ret_val; + break; } sf->cur_pc = pc; - ret_val = JS_ToPropertyKey(ctx, sp[-1]); - if (JS_IsException(ret_val)) + val = JS_GetPropertyValue(ctx, sp[-2], JS_DupValue(ctx, sp[-1])); + if (unlikely(JS_IsException(val))) goto exception; - JS_FreeValue(ctx, sp[-1]); - sp[-1] = ret_val; - break; } - sf->cur_pc = pc; - val = JS_GetPropertyValue(ctx, sp[-2], JS_DupValue(ctx, sp[-1])); *sp++ = val; - if (unlikely(JS_IsException(val))) - goto exception; } BREAK; @@ -18332,13 +19033,52 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, CASE(OP_put_array_el): { int ret; + JSObject *p; + uint32_t idx; - sf->cur_pc = pc; - ret = JS_SetPropertyValue(ctx, sp[-3], sp[-2], sp[-1], JS_PROP_THROW_STRICT); - JS_FreeValue(ctx, sp[-3]); - sp -= 3; - if (unlikely(ret < 0)) - goto exception; + if (likely(JS_VALUE_GET_TAG(sp[-3]) == JS_TAG_OBJECT && + JS_VALUE_GET_TAG(sp[-2]) == JS_TAG_INT)) { + p = JS_VALUE_GET_OBJ(sp[-3]); + idx = JS_VALUE_GET_INT(sp[-2]); + if (unlikely(p->class_id != JS_CLASS_ARRAY)) + goto put_array_el_slow_path; + if (unlikely(idx >= (uint32_t)p->u.array.count)) { + uint32_t new_len, array_len; + if (unlikely(idx != (uint32_t)p->u.array.count || + !p->fast_array || + !p->extensible || + p->shape->proto != JS_VALUE_GET_OBJ(ctx->class_proto[JS_CLASS_ARRAY]) || + !ctx->std_array_prototype)) { + goto put_array_el_slow_path; + } + if (likely(JS_VALUE_GET_TAG(p->prop[0].u.value) != JS_TAG_INT)) + goto put_array_el_slow_path; + /* cannot overflow otherwise the length would not be an integer */ + new_len = idx + 1; + if (unlikely(new_len > p->u.array.u1.size)) + goto put_array_el_slow_path; + array_len = JS_VALUE_GET_INT(p->prop[0].u.value); + if (new_len > array_len) { + if (unlikely(!(get_shape_prop(p->shape)->flags & JS_PROP_WRITABLE))) + goto put_array_el_slow_path; + p->prop[0].u.value = JS_NewInt32(ctx, new_len); + } + p->u.array.count = new_len; + p->u.array.u.values[idx] = sp[-1]; + } else { + set_value(ctx, &p->u.array.u.values[idx], sp[-1]); + } + JS_FreeValue(ctx, sp[-3]); + sp -= 3; + } else { + put_array_el_slow_path: + sf->cur_pc = pc; + ret = JS_SetPropertyValue(ctx, sp[-3], sp[-2], sp[-1], JS_PROP_THROW_STRICT); + JS_FreeValue(ctx, sp[-3]); + sp -= 3; + if (unlikely(ret < 0)) + goto exception; + } } BREAK; @@ -18493,12 +19233,10 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, *pv = __JS_NewFloat64(ctx, JS_VALUE_GET_FLOAT64(*pv) + JS_VALUE_GET_FLOAT64(op2)); sp--; - } else if (JS_VALUE_GET_TAG(*pv) == JS_TAG_STRING) { + } else if (JS_VALUE_GET_TAG(*pv) == JS_TAG_STRING && + JS_VALUE_GET_TAG(op2) == JS_TAG_STRING) { sp--; sf->cur_pc = pc; - op2 = JS_ToPrimitiveFree(ctx, op2, HINT_NONE); - if (JS_IsException(op2)) - goto exception; if (JS_ConcatStringInPlace(ctx, JS_VALUE_GET_STRING(*pv), op2)) { JS_FreeValue(ctx, op2); } else { @@ -18703,11 +19441,42 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, } BREAK; CASE(OP_post_inc): + { + JSValue op1; + int val; + op1 = sp[-1]; + if (JS_VALUE_GET_TAG(op1) == JS_TAG_INT) { + val = JS_VALUE_GET_INT(op1); + if (unlikely(val == INT32_MAX)) + goto post_inc_slow; + sp[0] = JS_NewInt32(ctx, val + 1); + } else { + post_inc_slow: + sf->cur_pc = pc; + if (js_post_inc_slow(ctx, sp, opcode)) + goto exception; + } + sp++; + } + BREAK; CASE(OP_post_dec): - sf->cur_pc = pc; - if (js_post_inc_slow(ctx, sp, opcode)) - goto exception; - sp++; + { + JSValue op1; + int val; + op1 = sp[-1]; + if (JS_VALUE_GET_TAG(op1) == JS_TAG_INT) { + val = JS_VALUE_GET_INT(op1); + if (unlikely(val == INT32_MIN)) + goto post_dec_slow; + sp[0] = JS_NewInt32(ctx, val - 1); + } else { + post_dec_slow: + sf->cur_pc = pc; + if (js_post_inc_slow(ctx, sp, opcode)) + goto exception; + } + sp++; + } BREAK; CASE(OP_inc_loc): { @@ -19339,7 +20108,7 @@ static JSValue JS_CallConstructorInternal(JSContext *ctx, goto not_a_function; p = JS_VALUE_GET_OBJ(func_obj); if (unlikely(!p->is_constructor)) - return JS_ThrowTypeError(ctx, "not a constructor"); + return JS_ThrowTypeErrorNotAConstructor(ctx, func_obj); if (unlikely(p->class_id != JS_CLASS_BYTECODE_FUNCTION)) { JSClassCall *call_func; call_func = ctx->rt->class_array[p->class_id].call; @@ -20888,6 +21657,7 @@ static __exception int js_parse_template_part(JSParseState *s, const uint8_t *p) { uint32_t c; StringBuffer b_s, *b = &b_s; + JSValue str; /* p points to the first byte of the template part */ if (string_buffer_init(s->ctx, b, 32)) @@ -20930,9 +21700,12 @@ static __exception int js_parse_template_part(JSParseState *s, const uint8_t *p) if (string_buffer_putc(b, c)) goto fail; } + str = string_buffer_end(b); + if (JS_IsException(str)) + return -1; s->token.val = TOK_TEMPLATE; s->token.u.str.sep = c; - s->token.u.str.str = string_buffer_end(b); + s->token.u.str.str = str; s->buf_ptr = p; return 0; @@ -20951,7 +21724,8 @@ static __exception int js_parse_string(JSParseState *s, int sep, uint32_t c; StringBuffer b_s, *b = &b_s; const uint8_t *p_escape; - + JSValue str; + /* string */ if (string_buffer_init(s->ctx, b, 32)) goto fail; @@ -20960,11 +21734,6 @@ static __exception int js_parse_string(JSParseState *s, int sep, goto invalid_char; c = *p; if (c < 0x20) { - if (!s->cur_func) { - if (do_throw) - js_parse_error_pos(s, p, "invalid character in a JSON string"); - goto fail; - } if (sep == '`') { if (c == '\r') { if (p[1] == '\n') @@ -21010,8 +21779,6 @@ static __exception int js_parse_string(JSParseState *s, int sep, continue; default: if (c >= '0' && c <= '9') { - if (!s->cur_func) - goto invalid_escape; /* JSON case */ if (!(s->cur_func->js_mode & JS_MODE_STRICT) && sep != '`') goto parse_escape; if (c == '0' && !(p[1] >= '0' && p[1] <= '9')) { @@ -21065,9 +21832,12 @@ static __exception int js_parse_string(JSParseState *s, int sep, if (string_buffer_putc(b, c)) goto fail; } + str = string_buffer_end(b); + if (JS_IsException(str)) + return -1; token->val = TOK_STRING; token->u.str.sep = c; - token->u.str.str = string_buffer_end(b); + token->u.str.str = str; *pp = p; return 0; @@ -21095,6 +21865,7 @@ static __exception int js_parse_regexp(JSParseState *s) StringBuffer b_s, *b = &b_s; StringBuffer b2_s, *b2 = &b2_s; uint32_t c; + JSValue body_str, flags_str; p = s->buf_ptr; p++; @@ -21176,9 +21947,17 @@ static __exception int js_parse_regexp(JSParseState *s) p = p_next; } + body_str = string_buffer_end(b); + flags_str = string_buffer_end(b2); + if (JS_IsException(body_str) || + JS_IsException(flags_str)) { + JS_FreeValue(s->ctx, body_str); + JS_FreeValue(s->ctx, flags_str); + return -1; + } s->token.val = TOK_REGEXP; - s->token.u.regexp.body = string_buffer_end(b); - s->token.u.regexp.flags = string_buffer_end(b2); + s->token.u.regexp.body = body_str; + s->token.u.regexp.flags = flags_str; s->buf_ptr = p; return 0; fail: @@ -21748,6 +22527,7 @@ static __exception int next_token(JSParseState *s) } /* 'c' is the first character. Return JS_ATOM_NULL in case of error */ +/* XXX: accept unicode identifiers as JSON5 ? */ static JSAtom json_parse_ident(JSParseState *s, const uint8_t **pp, int c) { const uint8_t *p; @@ -21780,6 +22560,178 @@ static JSAtom json_parse_ident(JSParseState *s, const uint8_t **pp, int c) return atom; } +static int json_parse_string(JSParseState *s, const uint8_t **pp, int sep) +{ + const uint8_t *p, *p_next; + int i; + uint32_t c; + StringBuffer b_s, *b = &b_s; + + if (string_buffer_init(s->ctx, b, 32)) + goto fail; + + p = *pp; + for(;;) { + if (p >= s->buf_end) { + goto end_of_input; + } + c = *p++; + if (c == sep) + break; + if (c < 0x20) { + js_parse_error_pos(s, p - 1, "Bad control character in string literal"); + goto fail; + } + if (c == '\\') { + c = *p++; + switch(c) { + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case '\\': break; + case '/': break; + case 'u': + c = 0; + for(i = 0; i < 4; i++) { + int h = from_hex(*p++); + if (h < 0) { + js_parse_error_pos(s, p - 1, "Bad Unicode escape"); + goto fail; + } + c = (c << 4) | h; + } + break; + case '\n': + if (s->ext_json) + continue; + goto bad_escape; + case 'v': + if (s->ext_json) { + c = '\v'; + break; + } + goto bad_escape; + default: + if (c == sep) + break; + if (p > s->buf_end) + goto end_of_input; + bad_escape: + js_parse_error_pos(s, p - 1, "Bad escaped character"); + goto fail; + } + } else + if (c >= 0x80) { + c = unicode_from_utf8(p - 1, UTF8_CHAR_LEN_MAX, &p_next); + if (c > 0x10FFFF) { + js_parse_error_pos(s, p - 1, "Bad UTF-8 sequence"); + goto fail; + } + p = p_next; + } + if (string_buffer_putc(b, c)) + goto fail; + } + s->token.val = TOK_STRING; + s->token.u.str.sep = sep; + s->token.u.str.str = string_buffer_end(b); + *pp = p; + return 0; + + end_of_input: + js_parse_error(s, "Unexpected end of JSON input"); + fail: + string_buffer_free(b); + return -1; +} + +static int json_parse_number(JSParseState *s, const uint8_t **pp) +{ + const uint8_t *p = *pp; + const uint8_t *p_start = p; + int radix; + double d; + JSATODTempMem atod_mem; + + if (*p == '+' || *p == '-') + p++; + + if (!is_digit(*p)) { + if (s->ext_json) { + if (strstart((const char *)p, "Infinity", (const char **)&p)) { + d = 1.0 / 0.0; + if (*p_start == '-') + d = -d; + goto done; + } else if (strstart((const char *)p, "NaN", (const char **)&p)) { + d = NAN; + goto done; + } else if (*p != '.') { + goto unexpected_token; + } + } else { + goto unexpected_token; + } + } + + if (p[0] == '0') { + if (s->ext_json) { + /* also accepts base 16, 8 and 2 prefix for integers */ + radix = 10; + if (p[1] == 'x' || p[1] == 'X') { + p += 2; + radix = 16; + } else if ((p[1] == 'o' || p[1] == 'O')) { + p += 2; + radix = 8; + } else if ((p[1] == 'b' || p[1] == 'B')) { + p += 2; + radix = 2; + } + if (radix != 10) { + /* prefix is present */ + if (to_digit(*p) >= radix) { + unexpected_token: + return js_parse_error_pos(s, p, "Unexpected token '%c'", *p); + } + d = js_atod((const char *)p_start, (const char **)&p, 0, + JS_ATOD_INT_ONLY | JS_ATOD_ACCEPT_BIN_OCT, &atod_mem); + goto done; + } + } + if (is_digit(p[1])) + return js_parse_error_pos(s, p, "Unexpected number"); + } + + while (is_digit(*p)) + p++; + + if (*p == '.') { + p++; + if (!is_digit(*p)) + return js_parse_error_pos(s, p, "Unterminated fractional number"); + while (is_digit(*p)) + p++; + } + if (*p == 'e' || *p == 'E') { + p++; + if (*p == '+' || *p == '-') + p++; + if (!is_digit(*p)) + return js_parse_error_pos(s, p, "Exponent part is missing a number"); + while (is_digit(*p)) + p++; + } + d = js_atod((const char *)p_start, NULL, 10, 0, &atod_mem); + done: + s->token.val = TOK_NUMBER; + s->token.u.num.val = JS_NewFloat64(s->ctx, d); + *pp = p; + return 0; +} + static __exception int json_next_token(JSParseState *s) { const uint8_t *p; @@ -21811,7 +22763,8 @@ static __exception int json_next_token(JSParseState *s) } /* fall through */ case '\"': - if (js_parse_string(s, c, TRUE, p + 1, &s->token, &p)) + p++; + if (json_parse_string(s, &p, c)) goto fail; break; case '\r': /* accept DOS and MAC newline sequences */ @@ -21901,7 +22854,6 @@ static __exception int json_next_token(JSParseState *s) case 'Y': case 'Z': case '_': case '$': - /* identifier : only pure ascii characters are accepted */ p++; atom = json_parse_ident(s, &p, c); if (atom == JS_ATOM_NULL) @@ -21912,39 +22864,23 @@ static __exception int json_next_token(JSParseState *s) s->token.val = TOK_IDENT; break; case '+': - if (!s->ext_json || !is_digit(p[1])) + if (!s->ext_json) goto def_token; goto parse_number; - case '0': - if (is_digit(p[1])) + case '.': + if (s->ext_json && is_digit(p[1])) + goto parse_number; + else goto def_token; - goto parse_number; case '-': - if (!is_digit(p[1])) - goto def_token; - goto parse_number; + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': /* number */ parse_number: - { - JSValue ret; - int flags, radix; - if (!s->ext_json) { - flags = 0; - radix = 10; - } else { - flags = ATOD_ACCEPT_BIN_OCT; - radix = 0; - } - ret = js_atof(s->ctx, (const char *)p, (const char **)&p, radix, - flags); - if (JS_IsException(ret)) - goto fail; - s->token.val = TOK_NUMBER; - s->token.u.num.val = ret; - } + if (json_parse_number(s, &p)) + goto fail; break; default: if (c >= 128) { @@ -22129,7 +23065,7 @@ BOOL JS_DetectModule(const char *input, size_t input_len) } static inline int get_prev_opcode(JSFunctionDef *fd) { - if (fd->last_opcode_pos < 0) + if (fd->last_opcode_pos < 0 || dbuf_error(&fd->byte_code)) return OP_invalid; else return fd->byte_code.buf[fd->last_opcode_pos]; @@ -22194,7 +23130,11 @@ static void emit_op(JSParseState *s, uint8_t val) static void emit_atom(JSParseState *s, JSAtom name) { - emit_u32(s, JS_DupAtom(s->ctx, name)); + DynBuf *bc = &s->cur_func->byte_code; + if (dbuf_claim(bc, 4)) + return; /* not enough memory : don't duplicate the atom */ + put_u32(bc->buf + bc->size, JS_DupAtom(s->ctx, name)); + bc->size += 4; } static int update_label(JSFunctionDef *s, int label, int delta) @@ -22208,29 +23148,33 @@ static int update_label(JSFunctionDef *s, int label, int delta) return ls->ref_count; } -static int new_label_fd(JSFunctionDef *fd, int label) +static int new_label_fd(JSFunctionDef *fd) { + int label; LabelSlot *ls; - if (label < 0) { - if (js_resize_array(fd->ctx, (void *)&fd->label_slots, - sizeof(fd->label_slots[0]), - &fd->label_size, fd->label_count + 1)) - return -1; - label = fd->label_count++; - ls = &fd->label_slots[label]; - ls->ref_count = 0; - ls->pos = -1; - ls->pos2 = -1; - ls->addr = -1; - ls->first_reloc = NULL; - } + if (js_resize_array(fd->ctx, (void *)&fd->label_slots, + sizeof(fd->label_slots[0]), + &fd->label_size, fd->label_count + 1)) + return -1; + label = fd->label_count++; + ls = &fd->label_slots[label]; + ls->ref_count = 0; + ls->pos = -1; + ls->pos2 = -1; + ls->addr = -1; + ls->first_reloc = NULL; return label; } static int new_label(JSParseState *s) { - return new_label_fd(s->cur_func, -1); + int label; + label = new_label_fd(s->cur_func); + if (unlikely(label < 0)) { + dbuf_set_error(&s->cur_func->byte_code); + } + return label; } /* don't update the last opcode and don't emit line number info */ @@ -22258,8 +23202,11 @@ static int emit_label(JSParseState *s, int label) static int emit_goto(JSParseState *s, int opcode, int label) { if (js_is_live_code(s)) { - if (label < 0) + if (label < 0) { label = new_label(s); + if (label < 0) + return -1; + } emit_op(s, opcode); emit_u32(s, label); s->cur_func->label_slots[label].ref_count++; @@ -23017,7 +23964,7 @@ static int __exception js_parse_property_name(JSParseState *s, } else if (s->token.val == '[') { if (next_token(s)) goto fail; - if (js_parse_expr(s)) + if (js_parse_assign_expr(s)) goto fail; if (js_parse_expect(s, ']')) goto fail; @@ -24218,18 +25165,19 @@ static __exception int js_parse_array_literal(JSParseState *s) return js_parse_expect(s, ']'); } -/* XXX: remove */ +/* check if scope chain contains a with statement */ static BOOL has_with_scope(JSFunctionDef *s, int scope_level) { - /* check if scope chain contains a with statement */ while (s) { - int scope_idx = s->scopes[scope_level].first; - while (scope_idx >= 0) { - JSVarDef *vd = &s->vars[scope_idx]; - - if (vd->var_name == JS_ATOM__with_) - return TRUE; - scope_idx = vd->scope_next; + /* no with in strict mode */ + if (!(s->js_mode & JS_MODE_STRICT)) { + int scope_idx = s->scopes[scope_level].first; + while (scope_idx >= 0) { + JSVarDef *vd = &s->vars[scope_idx]; + if (vd->var_name == JS_ATOM__with_) + return TRUE; + scope_idx = vd->scope_next; + } } /* check parent scopes */ scope_level = s->parent_scope_level; @@ -24262,7 +25210,11 @@ static __exception int get_lvalue(JSParseState *s, int *popcode, int *pscope, } if (name == JS_ATOM_this || name == JS_ATOM_new_target) goto invalid_lvalue; - depth = 2; /* will generate OP_get_ref_value */ + if (has_with_scope(fd, scope)) { + depth = 2; /* will generate OP_get_ref_value */ + } else { + depth = 0; + } break; case OP_get_field: name = get_u32(fd->byte_code.buf + fd->last_opcode_pos + 1); @@ -24299,14 +25251,22 @@ static __exception int get_lvalue(JSParseState *s, int *popcode, int *pscope, /* get the value but keep the object/fields on the stack */ switch(opcode) { case OP_scope_get_var: - label = new_label(s); - emit_op(s, OP_scope_make_ref); - emit_atom(s, name); - emit_u32(s, label); - emit_u16(s, scope); - update_label(fd, label, 1); - emit_op(s, OP_get_ref_value); - opcode = OP_get_ref_value; + if (depth != 0) { + label = new_label(s); + if (label < 0) + return -1; + emit_op(s, OP_scope_make_ref); + emit_atom(s, name); + emit_u32(s, label); + emit_u16(s, scope); + update_label(fd, label, 1); + emit_op(s, OP_get_ref_value); + opcode = OP_get_ref_value; + } else { + emit_op(s, OP_scope_get_var); + emit_atom(s, name); + emit_u16(s, scope); + } break; case OP_get_field: emit_op(s, OP_get_field2); @@ -24331,13 +25291,17 @@ static __exception int get_lvalue(JSParseState *s, int *popcode, int *pscope, } else { switch(opcode) { case OP_scope_get_var: - label = new_label(s); - emit_op(s, OP_scope_make_ref); - emit_atom(s, name); - emit_u32(s, label); - emit_u16(s, scope); - update_label(fd, label, 1); - opcode = OP_get_ref_value; + if (depth != 0) { + label = new_label(s); + if (label < 0) + return -1; + emit_op(s, OP_scope_make_ref); + emit_atom(s, name); + emit_u32(s, label); + emit_u16(s, scope); + update_label(fd, label, 1); + opcode = OP_get_ref_value; + } break; default: break; @@ -24371,6 +25335,21 @@ static void put_lvalue(JSParseState *s, int opcode, int scope, BOOL is_let) { switch(opcode) { + case OP_scope_get_var: + /* depth = 0 */ + switch(special) { + case PUT_LVALUE_NOKEEP: + case PUT_LVALUE_NOKEEP_DEPTH: + case PUT_LVALUE_KEEP_SECOND: + case PUT_LVALUE_NOKEEP_BOTTOM: + break; + case PUT_LVALUE_KEEP_TOP: + emit_op(s, OP_dup); + break; + default: + abort(); + } + break; case OP_get_field: case OP_scope_get_private_field: /* depth = 1 */ @@ -24442,8 +25421,6 @@ static void put_lvalue(JSParseState *s, int opcode, int scope, switch(opcode) { case OP_scope_get_var: /* val -- */ - assert(special == PUT_LVALUE_NOKEEP || - special == PUT_LVALUE_NOKEEP_DEPTH); emit_op(s, is_let ? OP_scope_put_var_init : OP_scope_put_var); emit_u32(s, name); /* has refcount */ emit_u16(s, scope); @@ -24797,6 +25774,8 @@ static int js_parse_destructuring_element(JSParseState *s, int tok, int is_arg, /* swap ref and lvalue object if any */ if (prop_name == JS_ATOM_NULL) { switch(depth_lvalue) { + case 0: + break; case 1: /* source prop x -> x source prop */ emit_op(s, OP_rot3r); @@ -24810,9 +25789,13 @@ static int js_parse_destructuring_element(JSParseState *s, int tok, int is_arg, emit_op(s, OP_rot5l); emit_op(s, OP_rot5l); break; + default: + abort(); } } else { switch(depth_lvalue) { + case 0: + break; case 1: /* source x -> x source */ emit_op(s, OP_swap); @@ -24825,6 +25808,8 @@ static int js_parse_destructuring_element(JSParseState *s, int tok, int is_arg, /* source x y z -> x y z source */ emit_op(s, OP_rot4l); break; + default: + abort(); } } } @@ -25341,6 +26326,23 @@ static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags) return js_parse_error(s, "invalid use of 'import()'"); if (js_parse_assign_expr(s)) return -1; + if (s->token.val == ',') { + if (next_token(s)) + return -1; + if (s->token.val != ')') { + if (js_parse_assign_expr(s)) + return -1; + /* accept a trailing comma */ + if (s->token.val == ',') { + if (next_token(s)) + return -1; + } + } else { + emit_op(s, OP_undefined); + } + } else { + emit_op(s, OP_undefined); + } if (js_parse_expect(s, ')')) return -1; emit_op(s, OP_import); @@ -25357,6 +26359,8 @@ static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags) BOOL has_optional_chain = FALSE; if (s->token.val == TOK_QUESTION_MARK_DOT) { + if ((parse_flags & PF_POSTFIX_CALL) == 0) + return js_parse_error(s, "new keyword cannot be used with an optional chain"); op_token_ptr = s->token.ptr; /* optional chaining */ if (next_token(s)) @@ -26457,7 +27461,7 @@ static __exception int js_parse_assign_expr2(JSParseState *s, int parse_flags) } if (op == '=') { - if (opcode == OP_get_ref_value && name == name0) { + if ((opcode == OP_get_ref_value || opcode == OP_scope_get_var) && name == name0) { set_object_name(s, name); } } else { @@ -26492,11 +27496,14 @@ static __exception int js_parse_assign_expr2(JSParseState *s, int parse_flags) return -1; } - if (opcode == OP_get_ref_value && name == name0) { + if ((opcode == OP_get_ref_value || opcode == OP_scope_get_var) && name == name0) { set_object_name(s, name); } switch(depth_lvalue) { + case 0: + emit_op(s, OP_dup); + break; case 1: emit_op(s, OP_insert2); break; @@ -27077,7 +28084,7 @@ static __exception int js_parse_for_in_of(JSParseState *s, int label_name, int chunk_size = pos_expr - pos_next; int offset = bc->size - pos_next; int i; - dbuf_realloc(bc, bc->size + chunk_size); + dbuf_claim(bc, chunk_size); dbuf_put(bc, bc->buf + pos_next, chunk_size); memset(bc->buf + pos_next, OP_nop, chunk_size); /* `next` part ends with a goto */ @@ -27483,7 +28490,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, int chunk_size = pos_body - pos_cont; int offset = bc->size - pos_cont; int i; - dbuf_realloc(bc, bc->size + chunk_size); + dbuf_claim(bc, chunk_size); dbuf_put(bc, bc->buf + pos_cont, chunk_size); memset(bc->buf + pos_cont, OP_nop, chunk_size); /* increment part ends with a goto */ @@ -27900,7 +28907,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s, return -1; } -/* 'name' is freed */ +/* 'name' is freed. The module is referenced by 'ctx->loaded_modules' */ static JSModuleDef *js_new_module_def(JSContext *ctx, JSAtom name) { JSModuleDef *m; @@ -27910,6 +28917,7 @@ static JSModuleDef *js_new_module_def(JSContext *ctx, JSAtom name) return NULL; } m->header.ref_count = 1; + add_gc_object(ctx->rt, &m->header, JS_GC_OBJ_TYPE_MODULE); m->module_name = name; m->module_ns = JS_UNDEFINED; m->func_obj = JS_UNDEFINED; @@ -27918,6 +28926,7 @@ static JSModuleDef *js_new_module_def(JSContext *ctx, JSAtom name) m->promise = JS_UNDEFINED; m->resolving_funcs[0] = JS_UNDEFINED; m->resolving_funcs[1] = JS_UNDEFINED; + m->private_value = JS_UNDEFINED; list_add_tail(&m->link, &ctx->loaded_modules); return m; } @@ -27927,6 +28936,11 @@ static void js_mark_module_def(JSRuntime *rt, JSModuleDef *m, { int i; + for(i = 0; i < m->req_module_entries_count; i++) { + JSReqModuleEntry *rme = &m->req_module_entries[i]; + JS_MarkValue(rt, rme->attributes, mark_func); + } + for(i = 0; i < m->export_entries_count; i++) { JSExportEntry *me = &m->export_entries[i]; if (me->export_type == JS_EXPORT_TYPE_LOCAL && @@ -27942,61 +28956,65 @@ static void js_mark_module_def(JSRuntime *rt, JSModuleDef *m, JS_MarkValue(rt, m->promise, mark_func); JS_MarkValue(rt, m->resolving_funcs[0], mark_func); JS_MarkValue(rt, m->resolving_funcs[1], mark_func); + JS_MarkValue(rt, m->private_value, mark_func); } -static void js_free_module_def(JSContext *ctx, JSModuleDef *m) +static void js_free_module_def(JSRuntime *rt, JSModuleDef *m) { int i; - JS_FreeAtom(ctx, m->module_name); + JS_FreeAtomRT(rt, m->module_name); for(i = 0; i < m->req_module_entries_count; i++) { JSReqModuleEntry *rme = &m->req_module_entries[i]; - JS_FreeAtom(ctx, rme->module_name); + JS_FreeAtomRT(rt, rme->module_name); + JS_FreeValueRT(rt, rme->attributes); } - js_free(ctx, m->req_module_entries); + js_free_rt(rt, m->req_module_entries); for(i = 0; i < m->export_entries_count; i++) { JSExportEntry *me = &m->export_entries[i]; if (me->export_type == JS_EXPORT_TYPE_LOCAL) - free_var_ref(ctx->rt, me->u.local.var_ref); - JS_FreeAtom(ctx, me->export_name); - JS_FreeAtom(ctx, me->local_name); + free_var_ref(rt, me->u.local.var_ref); + JS_FreeAtomRT(rt, me->export_name); + JS_FreeAtomRT(rt, me->local_name); } - js_free(ctx, m->export_entries); + js_free_rt(rt, m->export_entries); - js_free(ctx, m->star_export_entries); + js_free_rt(rt, m->star_export_entries); for(i = 0; i < m->import_entries_count; i++) { JSImportEntry *mi = &m->import_entries[i]; - JS_FreeAtom(ctx, mi->import_name); + JS_FreeAtomRT(rt, mi->import_name); } - js_free(ctx, m->import_entries); - js_free(ctx, m->async_parent_modules); + js_free_rt(rt, m->import_entries); + js_free_rt(rt, m->async_parent_modules); - JS_FreeValue(ctx, m->module_ns); - JS_FreeValue(ctx, m->func_obj); - JS_FreeValue(ctx, m->eval_exception); - JS_FreeValue(ctx, m->meta_obj); - JS_FreeValue(ctx, m->promise); - JS_FreeValue(ctx, m->resolving_funcs[0]); - JS_FreeValue(ctx, m->resolving_funcs[1]); - list_del(&m->link); - js_free(ctx, m); + JS_FreeValueRT(rt, m->module_ns); + JS_FreeValueRT(rt, m->func_obj); + JS_FreeValueRT(rt, m->eval_exception); + JS_FreeValueRT(rt, m->meta_obj); + JS_FreeValueRT(rt, m->promise); + JS_FreeValueRT(rt, m->resolving_funcs[0]); + JS_FreeValueRT(rt, m->resolving_funcs[1]); + JS_FreeValueRT(rt, m->private_value); + /* during the GC the finalizers are called in an arbitrary + order so the module may no longer be referenced by the JSContext list */ + if (m->link.next) { + list_del(&m->link); + } + remove_gc_object(&m->header); + if (rt->gc_phase == JS_GC_PHASE_REMOVE_CYCLES && m->header.ref_count != 0) { + list_add_tail(&m->header.link, &rt->gc_zero_ref_count_list); + } else { + js_free_rt(rt, m); + } } static int add_req_module_entry(JSContext *ctx, JSModuleDef *m, JSAtom module_name) { JSReqModuleEntry *rme; - int i; - - /* no need to add the module request if it is already present */ - for(i = 0; i < m->req_module_entries_count; i++) { - rme = &m->req_module_entries[i]; - if (rme->module_name == module_name) - return i; - } if (js_resize_array(ctx, (void **)&m->req_module_entries, sizeof(JSReqModuleEntry), @@ -28006,7 +29024,8 @@ static int add_req_module_entry(JSContext *ctx, JSModuleDef *m, rme = &m->req_module_entries[m->req_module_entries_count++]; rme->module_name = JS_DupAtom(ctx, module_name); rme->module = NULL; - return i; + rme->attributes = JS_UNDEFINED; + return m->req_module_entries_count - 1; } static JSExportEntry *find_export_entry(JSContext *ctx, JSModuleDef *m, @@ -28086,6 +29105,8 @@ JSModuleDef *JS_NewCModule(JSContext *ctx, const char *name_str, if (name == JS_ATOM_NULL) return NULL; m = js_new_module_def(ctx, name); + if (!m) + return NULL; m->init_func = func; return m; } @@ -28125,12 +29146,38 @@ int JS_SetModuleExport(JSContext *ctx, JSModuleDef *m, const char *export_name, return -1; } +int JS_SetModulePrivateValue(JSContext *ctx, JSModuleDef *m, JSValue val) +{ + set_value(ctx, &m->private_value, val); + return 0; +} + +JSValue JS_GetModulePrivateValue(JSContext *ctx, JSModuleDef *m) +{ + return JS_DupValue(ctx, m->private_value); +} + void JS_SetModuleLoaderFunc(JSRuntime *rt, JSModuleNormalizeFunc *module_normalize, JSModuleLoaderFunc *module_loader, void *opaque) { rt->module_normalize_func = module_normalize; - rt->module_loader_func = module_loader; + rt->module_loader_has_attr = FALSE; + rt->u.module_loader_func = module_loader; + rt->module_check_attrs = NULL; + rt->module_loader_opaque = opaque; +} + +void JS_SetModuleLoaderFunc2(JSRuntime *rt, + JSModuleNormalizeFunc *module_normalize, + JSModuleLoaderFunc2 *module_loader, + JSModuleCheckSupportedImportAttributes *module_check_attrs, + void *opaque) +{ + rt->module_normalize_func = module_normalize; + rt->module_loader_has_attr = TRUE; + rt->u.module_loader_func2 = module_loader; + rt->module_check_attrs = module_check_attrs; rt->module_loader_opaque = opaque; } @@ -28211,7 +29258,8 @@ static JSModuleDef *js_find_loaded_module(JSContext *ctx, JSAtom name) /* return NULL in case of exception (e.g. module could not be loaded) */ static JSModuleDef *js_host_resolve_imported_module(JSContext *ctx, const char *base_cname, - const char *cname1) + const char *cname1, + JSValueConst attributes) { JSRuntime *rt = ctx->rt; JSModuleDef *m; @@ -28244,22 +29292,26 @@ static JSModuleDef *js_host_resolve_imported_module(JSContext *ctx, JS_FreeAtom(ctx, module_name); /* load the module */ - if (!rt->module_loader_func) { + if (!rt->u.module_loader_func) { /* XXX: use a syntax error ? */ JS_ThrowReferenceError(ctx, "could not load module '%s'", cname); js_free(ctx, cname); return NULL; } - - m = rt->module_loader_func(ctx, cname, rt->module_loader_opaque); + if (rt->module_loader_has_attr) { + m = rt->u.module_loader_func2(ctx, cname, rt->module_loader_opaque, attributes); + } else { + m = rt->u.module_loader_func(ctx, cname, rt->module_loader_opaque); + } js_free(ctx, cname); return m; } static JSModuleDef *js_host_resolve_imported_module_atom(JSContext *ctx, - JSAtom base_module_name, - JSAtom module_name1) + JSAtom base_module_name, + JSAtom module_name1, + JSValueConst attributes) { const char *base_cname, *cname; JSModuleDef *m; @@ -28272,7 +29324,7 @@ static JSModuleDef *js_host_resolve_imported_module_atom(JSContext *ctx, JS_FreeCString(ctx, base_cname); return NULL; } - m = js_host_resolve_imported_module(ctx, base_cname, cname); + m = js_host_resolve_imported_module(ctx, base_cname, cname, attributes); JS_FreeCString(ctx, base_cname); JS_FreeCString(ctx, cname); return m; @@ -28683,6 +29735,7 @@ static JSValue js_build_module_ns(JSContext *ctx, JSModuleDef *m) en->export_name, JS_AUTOINIT_ID_MODULE_NS, m, JS_PROP_ENUMERABLE | JS_PROP_WRITABLE) < 0) + goto fail; break; default: break; @@ -28734,7 +29787,8 @@ static int js_resolve_module(JSContext *ctx, JSModuleDef *m) for(i = 0; i < m->req_module_entries_count; i++) { JSReqModuleEntry *rme = &m->req_module_entries[i]; m1 = js_host_resolve_imported_module_atom(ctx, m->module_name, - rme->module_name); + rme->module_name, + rme->attributes); if (!m1) return -1; rme->module = m1; @@ -28746,31 +29800,11 @@ static int js_resolve_module(JSContext *ctx, JSModuleDef *m) return 0; } -static JSVarRef *js_create_module_var(JSContext *ctx, BOOL is_lexical) -{ - JSVarRef *var_ref; - var_ref = js_malloc(ctx, sizeof(JSVarRef)); - if (!var_ref) - return NULL; - var_ref->header.ref_count = 1; - if (is_lexical) - var_ref->value = JS_UNINITIALIZED; - else - var_ref->value = JS_UNDEFINED; - var_ref->pvalue = &var_ref->value; - var_ref->is_detached = TRUE; - add_gc_object(ctx->rt, &var_ref->header, JS_GC_OBJ_TYPE_VAR_REF); - return var_ref; -} - /* Create the function associated with the module */ static int js_create_module_bytecode_function(JSContext *ctx, JSModuleDef *m) { JSFunctionBytecode *b; - int i; - JSVarRef **var_refs; JSValue func_obj, bfunc; - JSObject *p; bfunc = m->func_obj; func_obj = JS_NewObjectProtoClass(ctx, ctx->function_proto, @@ -28779,40 +29813,14 @@ static int js_create_module_bytecode_function(JSContext *ctx, JSModuleDef *m) if (JS_IsException(func_obj)) return -1; b = JS_VALUE_GET_PTR(bfunc); - - p = JS_VALUE_GET_OBJ(func_obj); - p->u.func.function_bytecode = b; - b->header.ref_count++; - p->u.func.home_object = NULL; - p->u.func.var_refs = NULL; - if (b->closure_var_count) { - var_refs = js_mallocz(ctx, sizeof(var_refs[0]) * b->closure_var_count); - if (!var_refs) - goto fail; - p->u.func.var_refs = var_refs; - - /* create the global variables. The other variables are - imported from other modules */ - for(i = 0; i < b->closure_var_count; i++) { - JSClosureVar *cv = &b->closure_var[i]; - JSVarRef *var_ref; - if (cv->is_local) { - var_ref = js_create_module_var(ctx, cv->is_lexical); - if (!var_ref) - goto fail; -#ifdef DUMP_MODULE_RESOLVE - printf("local %d: %p\n", i, var_ref); -#endif - var_refs[i] = var_ref; - } - } + func_obj = js_closure2(ctx, func_obj, b, NULL, NULL, TRUE, m); + if (JS_IsException(func_obj)) { + m->func_obj = JS_UNDEFINED; /* XXX: keep it ? */ + JS_FreeValue(ctx, func_obj); + return -1; } m->func_obj = func_obj; - JS_FreeValue(ctx, bfunc); return 0; - fail: - JS_FreeValue(ctx, func_obj); - return -1; } /* must be done before js_link_module() because of cyclic references */ @@ -28832,7 +29840,7 @@ static int js_create_module_function(JSContext *ctx, JSModuleDef *m) for(i = 0; i < m->export_entries_count; i++) { JSExportEntry *me = &m->export_entries[i]; if (me->export_type == JS_EXPORT_TYPE_LOCAL) { - var_ref = js_create_module_var(ctx, FALSE); + var_ref = js_create_var_ref(ctx, FALSE); if (!var_ref) return -1; me->u.local.var_ref = var_ref; @@ -28991,7 +29999,7 @@ static int js_inner_module_linking(JSContext *ctx, JSModuleDef *m, val = JS_GetModuleNamespace(ctx, m2); if (JS_IsException(val)) goto fail; - var_ref = js_create_module_var(ctx, TRUE); + var_ref = js_create_var_ref(ctx, TRUE); if (!var_ref) { JS_FreeValue(ctx, val); goto fail; @@ -29211,14 +30219,15 @@ static JSValue js_load_module_fulfilled(JSContext *ctx, JSValueConst this_val, static void JS_LoadModuleInternal(JSContext *ctx, const char *basename, const char *filename, - JSValueConst *resolving_funcs) + JSValueConst *resolving_funcs, + JSValueConst attributes) { JSValue evaluate_promise; JSModuleDef *m; JSValue ret, err, func_obj, evaluate_resolving_funcs[2]; JSValueConst func_data[3]; - m = js_host_resolve_imported_module(ctx, basename, filename); + m = js_host_resolve_imported_module(ctx, basename, filename, attributes); if (!m) goto fail; @@ -29265,7 +30274,7 @@ JSValue JS_LoadModule(JSContext *ctx, const char *basename, if (JS_IsException(promise)) return JS_EXCEPTION; JS_LoadModuleInternal(ctx, basename, filename, - (JSValueConst *)resolving_funcs); + (JSValueConst *)resolving_funcs, JS_UNDEFINED); JS_FreeValue(ctx, resolving_funcs[0]); JS_FreeValue(ctx, resolving_funcs[1]); return promise; @@ -29277,6 +30286,7 @@ static JSValue js_dynamic_import_job(JSContext *ctx, JSValueConst *resolving_funcs = argv; JSValueConst basename_val = argv[2]; JSValueConst specifier = argv[3]; + JSValueConst attributes = argv[4]; const char *basename = NULL, *filename; JSValue ret, err; @@ -29293,7 +30303,7 @@ static JSValue js_dynamic_import_job(JSContext *ctx, goto exception; JS_LoadModuleInternal(ctx, basename, filename, - resolving_funcs); + resolving_funcs, attributes); JS_FreeCString(ctx, filename); JS_FreeCString(ctx, basename); return JS_UNDEFINED; @@ -29307,11 +30317,12 @@ static JSValue js_dynamic_import_job(JSContext *ctx, return JS_UNDEFINED; } -static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier) +static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier, JSValueConst options) { JSAtom basename; - JSValue promise, resolving_funcs[2], basename_val; - JSValueConst args[4]; + JSValue promise, resolving_funcs[2], basename_val, err, ret; + JSValue specifier_str = JS_UNDEFINED, attributes = JS_UNDEFINED, attributes_obj = JS_UNDEFINED; + JSValueConst args[5]; basename = JS_GetScriptOrModuleName(ctx, 0); if (basename == JS_ATOM_NULL) @@ -29328,19 +30339,82 @@ static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier) return promise; } + /* the string conversion must occur here */ + specifier_str = JS_ToString(ctx, specifier); + if (JS_IsException(specifier_str)) + goto exception; + + if (!JS_IsUndefined(options)) { + if (!JS_IsObject(options)) { + JS_ThrowTypeError(ctx, "options must be an object"); + goto exception; + } + attributes_obj = JS_GetProperty(ctx, options, JS_ATOM_with); + if (JS_IsException(attributes_obj)) + goto exception; + if (!JS_IsUndefined(attributes_obj)) { + JSPropertyEnum *atoms; + uint32_t atoms_len, i; + JSValue val; + + if (!JS_IsObject(attributes_obj)) { + JS_ThrowTypeError(ctx, "options.with must be an object"); + goto exception; + } + attributes = JS_NewObjectProto(ctx, JS_NULL); + if (JS_GetOwnPropertyNamesInternal(ctx, &atoms, &atoms_len, JS_VALUE_GET_OBJ(attributes_obj), + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY)) { + goto exception; + } + for(i = 0; i < atoms_len; i++) { + val = JS_GetProperty(ctx, attributes_obj, atoms[i].atom); + if (JS_IsException(val)) + goto exception1; + if (!JS_IsString(val)) { + JS_FreeValue(ctx, val); + JS_ThrowTypeError(ctx, "module attribute values must be strings"); + goto exception1; + } + if (JS_DefinePropertyValue(ctx, attributes, atoms[i].atom, val, + JS_PROP_C_W_E) < 0) { + exception1: + JS_FreePropertyEnum(ctx, atoms, atoms_len); + goto exception; + } + } + JS_FreePropertyEnum(ctx, atoms, atoms_len); + if (ctx->rt->module_check_attrs && + ctx->rt->module_check_attrs(ctx, ctx->rt->module_loader_opaque, attributes) < 0) { + goto exception; + } + JS_FreeValue(ctx, attributes_obj); + } + } + args[0] = resolving_funcs[0]; args[1] = resolving_funcs[1]; args[2] = basename_val; - args[3] = specifier; - + args[3] = specifier_str; + args[4] = attributes; + /* cannot run JS_LoadModuleInternal synchronously because it would cause an unexpected recursion in js_evaluate_module() */ - JS_EnqueueJob(ctx, js_dynamic_import_job, 4, args); - + JS_EnqueueJob(ctx, js_dynamic_import_job, 5, args); + done: JS_FreeValue(ctx, basename_val); JS_FreeValue(ctx, resolving_funcs[0]); JS_FreeValue(ctx, resolving_funcs[1]); + JS_FreeValue(ctx, specifier_str); + JS_FreeValue(ctx, attributes); return promise; + exception: + JS_FreeValue(ctx, attributes_obj); + err = JS_GetException(ctx); + ret = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, + 1, (JSValueConst *)&err); + JS_FreeValue(ctx, ret); + JS_FreeValue(ctx, err); + goto done; } static void js_set_module_evaluated(JSContext *ctx, JSModuleDef *m) @@ -29417,6 +30491,14 @@ static int exec_module_list_cmp(const void *p1, const void *p2, void *opaque) static int js_execute_async_module(JSContext *ctx, JSModuleDef *m); static int js_execute_sync_module(JSContext *ctx, JSModuleDef *m, JSValue *pvalue); +#ifdef DUMP_MODULE_EXEC +static void js_dump_module(JSContext *ctx, const char *str, JSModuleDef *m) +{ + char buf1[ATOM_GET_STR_BUF_SIZE]; + static const char *module_status_str[] = { "unlinked", "linking", "linked", "evaluating", "evaluating_async", "evaluated" }; + printf("%s: %s status=%s\n", str, JS_AtomGetStr(ctx, buf1, sizeof(buf1), m->module_name), module_status_str[m->status]); +} +#endif static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data) @@ -29425,6 +30507,9 @@ static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst t JSValueConst error = argv[0]; int i; +#ifdef DUMP_MODULE_EXEC + js_dump_module(ctx, __func__, module); +#endif if (js_check_stack_overflow(ctx->rt, 0)) return JS_ThrowStackOverflow(ctx); @@ -29440,14 +30525,7 @@ static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst t module->eval_has_exception = TRUE; module->eval_exception = JS_DupValue(ctx, error); module->status = JS_MODULE_STATUS_EVALUATED; - - for(i = 0; i < module->async_parent_modules_count; i++) { - JSModuleDef *m = module->async_parent_modules[i]; - JSValue m_obj = JS_NewModuleValue(ctx, m); - js_async_module_execution_rejected(ctx, JS_UNDEFINED, 1, &error, 0, - &m_obj); - JS_FreeValue(ctx, m_obj); - } + module->async_evaluation = FALSE; if (!JS_IsUndefined(module->promise)) { JSValue ret_val; @@ -29456,6 +30534,14 @@ static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst t 1, &error); JS_FreeValue(ctx, ret_val); } + + for(i = 0; i < module->async_parent_modules_count; i++) { + JSModuleDef *m = module->async_parent_modules[i]; + JSValue m_obj = JS_NewModuleValue(ctx, m); + js_async_module_execution_rejected(ctx, JS_UNDEFINED, 1, &error, 0, + &m_obj); + JS_FreeValue(ctx, m_obj); + } return JS_UNDEFINED; } @@ -29466,6 +30552,9 @@ static JSValue js_async_module_execution_fulfilled(JSContext *ctx, JSValueConst ExecModuleList exec_list_s, *exec_list = &exec_list_s; int i; +#ifdef DUMP_MODULE_EXEC + js_dump_module(ctx, __func__, module); +#endif if (module->status == JS_MODULE_STATUS_EVALUATED) { assert(module->eval_has_exception); return JS_UNDEFINED; @@ -29491,6 +30580,9 @@ static JSValue js_async_module_execution_fulfilled(JSContext *ctx, JSValueConst for(i = 0; i < exec_list->count; i++) { JSModuleDef *m = exec_list->tab[i]; +#ifdef DUMP_MODULE_EXEC + printf(" %d/%d", i, exec_list->count); js_dump_module(ctx, "", m); +#endif if (m->status == JS_MODULE_STATUS_EVALUATED) { assert(m->eval_has_exception); } else if (m->has_tla) { @@ -29505,6 +30597,7 @@ static JSValue js_async_module_execution_fulfilled(JSContext *ctx, JSValueConst JS_FreeValue(ctx, m_obj); JS_FreeValue(ctx, error); } else { + m->async_evaluation = FALSE; js_set_module_evaluated(ctx, m); } } @@ -29517,6 +30610,9 @@ static int js_execute_async_module(JSContext *ctx, JSModuleDef *m) { JSValue promise, m_obj; JSValue resolve_funcs[2], ret_val; +#ifdef DUMP_MODULE_EXEC + js_dump_module(ctx, __func__, m); +#endif promise = js_async_function_call(ctx, m->func_obj, JS_UNDEFINED, 0, NULL, 0); if (JS_IsException(promise)) return -1; @@ -29536,6 +30632,9 @@ static int js_execute_async_module(JSContext *ctx, JSModuleDef *m) static int js_execute_sync_module(JSContext *ctx, JSModuleDef *m, JSValue *pvalue) { +#ifdef DUMP_MODULE_EXEC + js_dump_module(ctx, __func__, m); +#endif if (m->init_func) { /* C module init : no asynchronous execution */ if (m->init_func(ctx, m) < 0) @@ -29575,19 +30674,16 @@ static int js_inner_module_evaluation(JSContext *ctx, JSModuleDef *m, JSModuleDef *m1; int i; +#ifdef DUMP_MODULE_EXEC + js_dump_module(ctx, __func__, m); +#endif + if (js_check_stack_overflow(ctx->rt, 0)) { JS_ThrowStackOverflow(ctx); *pvalue = JS_GetException(ctx); return -1; } -#ifdef DUMP_MODULE_RESOLVE - { - char buf1[ATOM_GET_STR_BUF_SIZE]; - printf("js_inner_module_evaluation '%s':\n", JS_AtomGetStr(ctx, buf1, sizeof(buf1), m->module_name)); - } -#endif - if (m->status == JS_MODULE_STATUS_EVALUATING_ASYNC || m->status == JS_MODULE_STATUS_EVALUATED) { if (m->eval_has_exception) { @@ -29688,6 +30784,9 @@ static JSValue js_evaluate_module(JSContext *ctx, JSModuleDef *m) JSModuleDef *m1, *stack_top; JSValue ret_val, result; +#ifdef DUMP_MODULE_EXEC + js_dump_module(ctx, __func__, m); +#endif assert(m->status == JS_MODULE_STATUS_LINKED || m->status == JS_MODULE_STATUS_EVALUATING_ASYNC || m->status == JS_MODULE_STATUS_EVALUATED); @@ -29720,6 +30819,9 @@ static JSValue js_evaluate_module(JSContext *ctx, JSModuleDef *m) 1, (JSValueConst *)&m->eval_exception); JS_FreeValue(ctx, ret_val); } else { +#ifdef DUMP_MODULE_EXEC + js_dump_module(ctx, " done", m); +#endif assert(m->status == JS_MODULE_STATUS_EVALUATING_ASYNC || m->status == JS_MODULE_STATUS_EVALUATED); assert(!m->eval_has_exception); @@ -29736,27 +30838,109 @@ static JSValue js_evaluate_module(JSContext *ctx, JSModuleDef *m) return JS_DupValue(ctx, m->promise); } -static __exception JSAtom js_parse_from_clause(JSParseState *s) +static __exception int js_parse_with_clause(JSParseState *s, JSReqModuleEntry *rme) +{ + JSContext *ctx = s->ctx; + JSAtom key; + int ret; + const uint8_t *key_token_ptr; + + if (next_token(s)) + return -1; + if (js_parse_expect(s, '{')) + return -1; + while (s->token.val != '}') { + key_token_ptr = s->token.ptr; + if (s->token.val == TOK_STRING) { + key = JS_ValueToAtom(ctx, s->token.u.str.str); + if (key == JS_ATOM_NULL) + return -1; + } else { + if (!token_is_ident(s->token.val)) { + js_parse_error(s, "identifier expected"); + return -1; + } + key = JS_DupAtom(ctx, s->token.u.ident.atom); + } + if (next_token(s)) + return -1; + if (js_parse_expect(s, ':')) { + JS_FreeAtom(ctx, key); + return -1; + } + if (s->token.val != TOK_STRING) { + js_parse_error_pos(s, key_token_ptr, "string expected"); + return -1; + } + if (JS_IsUndefined(rme->attributes)) { + JSValue attributes = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(attributes)) { + JS_FreeAtom(ctx, key); + return -1; + } + rme->attributes = attributes; + } + ret = JS_HasProperty(ctx, rme->attributes, key); + if (ret != 0) { + JS_FreeAtom(ctx, key); + if (ret < 0) + return -1; + else + return js_parse_error(s, "duplicate with key"); + } + ret = JS_DefinePropertyValue(ctx, rme->attributes, key, + JS_DupValue(ctx, s->token.u.str.str), JS_PROP_C_W_E); + JS_FreeAtom(ctx, key); + if (ret < 0) + return -1; + if (next_token(s)) + return -1; + if (s->token.val != ',') + break; + if (next_token(s)) + return -1; + } + if (!JS_IsUndefined(rme->attributes) && + ctx->rt->module_check_attrs && + ctx->rt->module_check_attrs(ctx, ctx->rt->module_loader_opaque, rme->attributes) < 0) { + return -1; + } + return js_parse_expect(s, '}'); +} + +/* return the module index in m->req_module_entries[] or < 0 if error */ +static __exception int js_parse_from_clause(JSParseState *s, JSModuleDef *m) { JSAtom module_name; + int idx; + if (!token_is_pseudo_keyword(s, JS_ATOM_from)) { js_parse_error(s, "from clause expected"); - return JS_ATOM_NULL; + return -1; } if (next_token(s)) - return JS_ATOM_NULL; + return -1; if (s->token.val != TOK_STRING) { js_parse_error(s, "string expected"); - return JS_ATOM_NULL; + return -1; } module_name = JS_ValueToAtom(s->ctx, s->token.u.str.str); if (module_name == JS_ATOM_NULL) - return JS_ATOM_NULL; + return -1; if (next_token(s)) { JS_FreeAtom(s->ctx, module_name); - return JS_ATOM_NULL; + return -1; + } + + idx = add_req_module_entry(s->ctx, m, module_name); + JS_FreeAtom(s->ctx, module_name); + if (idx < 0) + return -1; + if (s->token.val == TOK_WITH) { + if (js_parse_with_clause(s, &m->req_module_entries[idx])) + return -1; } - return module_name; + return idx; } static __exception int js_parse_export(JSParseState *s) @@ -29765,7 +30949,6 @@ static __exception int js_parse_export(JSParseState *s) JSModuleDef *m = s->cur_func->module; JSAtom local_name, export_name; int first_export, idx, i, tok; - JSAtom module_name; JSExportEntry *me; if (next_token(s)) @@ -29840,11 +31023,7 @@ static __exception int js_parse_export(JSParseState *s) if (js_parse_expect(s, '}')) return -1; if (token_is_pseudo_keyword(s, JS_ATOM_from)) { - module_name = js_parse_from_clause(s); - if (module_name == JS_ATOM_NULL) - return -1; - idx = add_req_module_entry(ctx, m, module_name); - JS_FreeAtom(ctx, module_name); + idx = js_parse_from_clause(s, m); if (idx < 0) return -1; for(i = first_export; i < m->export_entries_count; i++) { @@ -29866,11 +31045,7 @@ static __exception int js_parse_export(JSParseState *s) export_name = JS_DupAtom(ctx, s->token.u.ident.atom); if (next_token(s)) goto fail1; - module_name = js_parse_from_clause(s); - if (module_name == JS_ATOM_NULL) - goto fail1; - idx = add_req_module_entry(ctx, m, module_name); - JS_FreeAtom(ctx, module_name); + idx = js_parse_from_clause(s, m); if (idx < 0) goto fail1; me = add_export_entry(s, m, JS_ATOM__star_, export_name, @@ -29880,11 +31055,7 @@ static __exception int js_parse_export(JSParseState *s) return -1; me->u.req_module_idx = idx; } else { - module_name = js_parse_from_clause(s); - if (module_name == JS_ATOM_NULL) - return -1; - idx = add_req_module_entry(ctx, m, module_name); - JS_FreeAtom(ctx, module_name); + idx = js_parse_from_clause(s, m); if (idx < 0) return -1; if (add_star_export_entry(ctx, m, idx) < 0) @@ -29932,7 +31103,7 @@ static __exception int js_parse_export(JSParseState *s) } static int add_closure_var(JSContext *ctx, JSFunctionDef *s, - BOOL is_local, BOOL is_arg, + JSClosureTypeEnum closure_type, int var_idx, JSAtom var_name, BOOL is_const, BOOL is_lexical, JSVarKindEnum var_kind); @@ -29954,9 +31125,10 @@ static int add_import(JSParseState *s, JSModuleDef *m, } } - var_idx = add_closure_var(ctx, s->cur_func, is_star, FALSE, + var_idx = add_closure_var(ctx, s->cur_func, + is_star ? JS_CLOSURE_MODULE_DECL : JS_CLOSURE_MODULE_IMPORT, m->import_entries_count, - local_name, TRUE, TRUE, FALSE); + local_name, TRUE, TRUE, JS_VAR_NORMAL); if (var_idx < 0) return -1; if (js_resize_array(ctx, (void **)&m->import_entries, @@ -29990,6 +31162,14 @@ static __exception int js_parse_import(JSParseState *s) JS_FreeAtom(ctx, module_name); return -1; } + idx = add_req_module_entry(ctx, m, module_name); + JS_FreeAtom(ctx, module_name); + if (idx < 0) + return -1; + if (s->token.val == TOK_WITH) { + if (js_parse_with_clause(s, &m->req_module_entries[idx])) + return -1; + } } else { if (s->token.val == TOK_IDENT) { if (s->token.u.ident.is_reserved) { @@ -30088,14 +31268,10 @@ static __exception int js_parse_import(JSParseState *s) return -1; } end_import_clause: - module_name = js_parse_from_clause(s); - if (module_name == JS_ATOM_NULL) + idx = js_parse_from_clause(s, m); + if (idx < 0) return -1; } - idx = add_req_module_entry(ctx, m, module_name); - JS_FreeAtom(ctx, module_name); - if (idx < 0) - return -1; for(i = first_import; i < m->import_entries_count; i++) m->import_entries[i].req_module_idx = idx; @@ -30174,7 +31350,7 @@ static JSFunctionDef *js_new_function_def(JSContext *ctx, fd->is_eval = is_eval; fd->is_func_expr = is_func_expr; - js_dbuf_init(ctx, &fd->byte_code); + js_dbuf_bytecode_init(ctx, &fd->byte_code); fd->last_opcode_pos = -1; fd->func_name = JS_ATOM_NULL; fd->var_object_idx = -1; @@ -30232,6 +31408,8 @@ static void free_bytecode_atoms(JSRuntime *rt, case OP_FMT_atom_u16: case OP_FMT_atom_label_u8: case OP_FMT_atom_label_u16: + if ((pos + 1 + 4) > bc_len) + break; /* may happen if there is not enough memory when emiting bytecode */ atom = get_u32(bc_buf + pos + 1); JS_FreeAtomRT(rt, atom); break; @@ -30522,7 +31700,7 @@ static void dump_byte_code(JSContext *ctx, int pass, has_pool_idx: printf(" %u: ", idx); if (idx < cpool_count) { - JS_PrintValue(ctx, stdout, cpool[idx], NULL); + JS_PrintValue(ctx, js_dump_value_write, stdout, cpool[idx], NULL); } break; case OP_FMT_atom: @@ -30714,12 +31892,39 @@ static __maybe_unused void js_dump_function_bytecode(JSContext *ctx, JSFunctionB printf(" closure vars:\n"); for(i = 0; i < b->closure_var_count; i++) { JSClosureVar *cv = &b->closure_var[i]; - printf("%5d: %s %s:%s%d %s\n", i, - JS_AtomGetStr(ctx, atom_buf, sizeof(atom_buf), cv->var_name), - cv->is_local ? "local" : "parent", - cv->is_arg ? "arg" : "loc", cv->var_idx, + printf("%5d: %s %s", i, cv->is_const ? "const" : - cv->is_lexical ? "let" : "var"); + cv->is_lexical ? "let" : "var", + JS_AtomGetStr(ctx, atom_buf, sizeof(atom_buf), cv->var_name)); + switch(cv->closure_type) { + case JS_CLOSURE_LOCAL: + printf(" [loc%d]\n", cv->var_idx); + break; + case JS_CLOSURE_ARG: + printf(" [arg%d]\n", cv->var_idx); + break; + case JS_CLOSURE_REF: + printf(" [ref%d]\n", cv->var_idx); + break; + case JS_CLOSURE_GLOBAL_REF: + printf(" [global_ref%d]\n", cv->var_idx); + break; + case JS_CLOSURE_GLOBAL_DECL: + printf(" [global_decl]\n"); + break; + case JS_CLOSURE_GLOBAL: + printf(" [global]\n"); + break; + case JS_CLOSURE_MODULE_DECL: + printf(" [module_decl]\n"); + break; + case JS_CLOSURE_MODULE_IMPORT: + printf(" [module_import]\n"); + break; + default: + printf(" [?]\n"); + break; + } } } printf(" stack_size: %d\n", b->stack_size); @@ -30740,7 +31945,7 @@ static __maybe_unused void js_dump_function_bytecode(JSContext *ctx, JSFunctionB #endif static int add_closure_var(JSContext *ctx, JSFunctionDef *s, - BOOL is_local, BOOL is_arg, + JSClosureTypeEnum closure_type, int var_idx, JSAtom var_name, BOOL is_const, BOOL is_lexical, JSVarKindEnum var_kind) @@ -30758,8 +31963,7 @@ static int add_closure_var(JSContext *ctx, JSFunctionDef *s, &s->closure_var_size, s->closure_var_count + 1)) return -1; cv = &s->closure_var[s->closure_var_count++]; - cv->is_local = is_local; - cv->is_arg = is_arg; + cv->closure_type = closure_type; cv->is_const = is_const; cv->is_lexical = is_lexical; cv->var_kind = var_kind; @@ -30780,46 +31984,34 @@ static int find_closure_var(JSContext *ctx, JSFunctionDef *s, return -1; } -/* 'fd' must be a parent of 's'. Create in 's' a closure referencing a - local variable (is_local = TRUE) or a closure (is_local = FALSE) in - 'fd' */ -static int get_closure_var2(JSContext *ctx, JSFunctionDef *s, - JSFunctionDef *fd, BOOL is_local, - BOOL is_arg, int var_idx, JSAtom var_name, - BOOL is_const, BOOL is_lexical, - JSVarKindEnum var_kind) +/* 'fd' must be a parent of 's'. Create in 's' a closure referencing + another one in 'fd' */ +static int get_closure_var(JSContext *ctx, JSFunctionDef *s, + JSFunctionDef *fd, JSClosureTypeEnum closure_type, + int var_idx, JSAtom var_name, + BOOL is_const, BOOL is_lexical, + JSVarKindEnum var_kind) { int i; if (fd != s->parent) { - var_idx = get_closure_var2(ctx, s->parent, fd, is_local, - is_arg, var_idx, var_name, - is_const, is_lexical, var_kind); + var_idx = get_closure_var(ctx, s->parent, fd, closure_type, + var_idx, var_name, + is_const, is_lexical, var_kind); if (var_idx < 0) return -1; - is_local = FALSE; + if (closure_type != JS_CLOSURE_GLOBAL_REF) + closure_type = JS_CLOSURE_REF; } for(i = 0; i < s->closure_var_count; i++) { JSClosureVar *cv = &s->closure_var[i]; - if (cv->var_idx == var_idx && cv->is_arg == is_arg && - cv->is_local == is_local) + if (cv->var_idx == var_idx && cv->closure_type == closure_type) return i; } - return add_closure_var(ctx, s, is_local, is_arg, var_idx, var_name, + return add_closure_var(ctx, s, closure_type, var_idx, var_name, is_const, is_lexical, var_kind); } -static int get_closure_var(JSContext *ctx, JSFunctionDef *s, - JSFunctionDef *fd, BOOL is_arg, - int var_idx, JSAtom var_name, - BOOL is_const, BOOL is_lexical, - JSVarKindEnum var_kind) -{ - return get_closure_var2(ctx, s, fd, TRUE, is_arg, - var_idx, var_name, is_const, is_lexical, - var_kind); -} - static int get_with_scope_opcode(int op) { if (op == OP_scope_get_var_undef) @@ -30892,75 +32084,6 @@ static int optimize_scope_make_ref(JSContext *ctx, JSFunctionDef *s, return pos_next; } -static int optimize_scope_make_global_ref(JSContext *ctx, JSFunctionDef *s, - DynBuf *bc, uint8_t *bc_buf, - LabelSlot *ls, int pos_next, - JSAtom var_name) -{ - int label_pos, end_pos, pos, op; - BOOL is_strict; - is_strict = ((s->js_mode & JS_MODE_STRICT) != 0); - - /* replace the reference get/put with normal variable - accesses */ - if (is_strict) { - /* need to check if the variable exists before evaluating the right - expression */ - /* XXX: need an extra OP_true if destructuring an array */ - dbuf_putc(bc, OP_check_var); - dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - } else { - /* XXX: need 2 extra OP_true if destructuring an array */ - } - if (bc_buf[pos_next] == OP_get_ref_value) { - dbuf_putc(bc, OP_get_var); - dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - pos_next++; - } - /* remove the OP_label to make room for replacement */ - /* label should have a refcount of 0 anyway */ - /* XXX: should have emitted several OP_nop to avoid this kludge */ - label_pos = ls->pos; - pos = label_pos - 5; - assert(bc_buf[pos] == OP_label); - end_pos = label_pos + 2; - op = bc_buf[label_pos]; - if (is_strict) { - if (op != OP_nop) { - switch(op) { - case OP_insert3: - op = OP_insert2; - break; - case OP_perm4: - op = OP_perm3; - break; - case OP_rot3l: - op = OP_swap; - break; - default: - abort(); - } - bc_buf[pos++] = op; - } - } else { - if (op == OP_insert3) - bc_buf[pos++] = OP_dup; - } - if (is_strict) { - bc_buf[pos] = OP_put_var_strict; - /* XXX: need 1 extra OP_drop if destructuring an array */ - } else { - bc_buf[pos] = OP_put_var; - /* XXX: need 2 extra OP_drop if destructuring an array */ - } - put_u32(bc_buf + pos + 1, JS_DupAtom(ctx, var_name)); - pos += 5; - /* pad with OP_nop */ - while (pos < end_pos) - bc_buf[pos++] = OP_nop; - return pos_next; -} - static int add_var_this(JSContext *ctx, JSFunctionDef *fd) { int idx; @@ -31021,14 +32144,20 @@ static void var_object_test(JSContext *ctx, JSFunctionDef *s, { dbuf_putc(bc, get_with_scope_opcode(op)); dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - *plabel_done = new_label_fd(s, *plabel_done); + if (*plabel_done < 0) { + *plabel_done = new_label_fd(s); + if (*plabel_done < 0) { + dbuf_set_error(bc); + return; + } + } dbuf_put_u32(bc, *plabel_done); dbuf_putc(bc, is_with); update_label(s, *plabel_done, 1); s->jump_size++; } -/* return the position of the next opcode */ +/* return the position of the next opcode or -1 if error */ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, JSAtom var_name, int scope_level, int op, DynBuf *bc, uint8_t *bc_buf, @@ -31147,14 +32276,22 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, } } break; + case OP_scope_put_var: + if (!(var_idx & ARGUMENT_VAR_OFFSET) && + s->vars[var_idx].var_kind == JS_VAR_FUNCTION_NAME) { + /* in non strict mode, modifying the function name is ignored */ + dbuf_putc(bc, OP_drop); + goto done; + } + goto local_scope_var; case OP_scope_get_ref: dbuf_putc(bc, OP_undefined); - /* fall thru */ + goto local_scope_var; case OP_scope_get_var_checkthis: case OP_scope_get_var_undef: case OP_scope_get_var: - case OP_scope_put_var: case OP_scope_put_var_init: + local_scope_var: is_put = (op == OP_scope_put_var || op == OP_scope_put_var_init); if (var_idx & ARGUMENT_VAR_OFFSET) { dbuf_putc(bc, OP_get_arg + is_put); @@ -31227,7 +32364,7 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, break; } else if (vd->var_name == JS_ATOM__with_ && !is_pseudo_var) { vd->is_captured = 1; - idx = get_closure_var(ctx, s, fd, FALSE, idx, vd->var_name, FALSE, FALSE, JS_VAR_NORMAL); + idx = get_closure_var(ctx, s, fd, JS_CLOSURE_LOCAL, idx, vd->var_name, FALSE, FALSE, JS_VAR_NORMAL); if (idx >= 0) { dbuf_putc(bc, OP_get_var_ref); dbuf_put_u16(bc, idx); @@ -31264,7 +32401,7 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, if (!is_arg_scope && fd->var_object_idx >= 0 && !is_pseudo_var) { vd = &fd->vars[fd->var_object_idx]; vd->is_captured = 1; - idx = get_closure_var(ctx, s, fd, FALSE, + idx = get_closure_var(ctx, s, fd, JS_CLOSURE_LOCAL, fd->var_object_idx, vd->var_name, FALSE, FALSE, JS_VAR_NORMAL); dbuf_putc(bc, OP_get_var_ref); @@ -31276,7 +32413,7 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, if (fd->arg_var_object_idx >= 0 && !is_pseudo_var) { vd = &fd->vars[fd->arg_var_object_idx]; vd->is_captured = 1; - idx = get_closure_var(ctx, s, fd, FALSE, + idx = get_closure_var(ctx, s, fd, JS_CLOSURE_LOCAL, fd->arg_var_object_idx, vd->var_name, FALSE, FALSE, JS_VAR_NORMAL); dbuf_putc(bc, OP_get_var_ref); @@ -31298,25 +32435,37 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, JSClosureVar *cv = &fd->closure_var[idx1]; if (var_name == cv->var_name) { if (fd != s) { - idx = get_closure_var2(ctx, s, fd, - FALSE, - cv->is_arg, idx1, - cv->var_name, cv->is_const, - cv->is_lexical, cv->var_kind); + JSClosureTypeEnum closure_type; + if (cv->closure_type == JS_CLOSURE_GLOBAL || + cv->closure_type == JS_CLOSURE_GLOBAL_DECL || + cv->closure_type == JS_CLOSURE_GLOBAL_REF) + closure_type = JS_CLOSURE_GLOBAL_REF; + else + closure_type = JS_CLOSURE_REF; + idx = get_closure_var(ctx, s, fd, + closure_type, + idx1, + cv->var_name, cv->is_const, + cv->is_lexical, cv->var_kind); } else { idx = idx1; } - goto has_idx; + if (cv->closure_type == JS_CLOSURE_GLOBAL || + cv->closure_type == JS_CLOSURE_GLOBAL_DECL || + cv->closure_type == JS_CLOSURE_GLOBAL_REF) + goto has_global_idx; + else + goto has_idx; } else if ((cv->var_name == JS_ATOM__var_ || cv->var_name == JS_ATOM__arg_var_ || cv->var_name == JS_ATOM__with_) && !is_pseudo_var) { int is_with = (cv->var_name == JS_ATOM__with_); if (fd != s) { - idx = get_closure_var2(ctx, s, fd, - FALSE, - cv->is_arg, idx1, - cv->var_name, FALSE, FALSE, - JS_VAR_NORMAL); + idx = get_closure_var(ctx, s, fd, + JS_CLOSURE_REF, + idx1, + cv->var_name, FALSE, FALSE, + JS_VAR_NORMAL); } else { idx = idx1; } @@ -31325,19 +32474,67 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, var_object_test(ctx, s, var_name, op, bc, &label_done, is_with); } } - } - if (var_idx >= 0) { + /* not found: add a closure for a global variable access */ + idx1 = add_closure_var(ctx, fd, JS_CLOSURE_GLOBAL, 0, var_name, + FALSE, FALSE, JS_VAR_NORMAL); + if (idx1 < 0) + return -1; + if (fd != s) { + idx = get_closure_var(ctx, s, fd, + JS_CLOSURE_GLOBAL_REF, + idx1, + var_name, FALSE, FALSE, + JS_VAR_NORMAL); + } else { + idx = idx1; + } + has_global_idx: + /* global variable access */ + switch (op) { + case OP_scope_make_ref: + if (label_done == -1 && can_opt_put_global_ref_value(bc_buf, ls->pos)) { + pos_next = optimize_scope_make_ref(ctx, s, bc, bc_buf, ls, + pos_next, + OP_get_var, idx); + } else { + dbuf_putc(bc, OP_make_var_ref); + dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); + } + break; + case OP_scope_get_ref: + /* XXX: should create a dummy object with a named slot that is + a reference to the global variable */ + dbuf_putc(bc, OP_undefined); + dbuf_putc(bc, OP_get_var); + dbuf_put_u16(bc, idx); + break; + case OP_scope_get_var_undef: + case OP_scope_get_var: + case OP_scope_put_var: + dbuf_putc(bc, OP_get_var_undef + (op - OP_scope_get_var_undef)); + dbuf_put_u16(bc, idx); + break; + case OP_scope_put_var_init: + dbuf_putc(bc, OP_put_var_init); + dbuf_put_u16(bc, idx); + break; + case OP_scope_delete_var: + dbuf_putc(bc, OP_delete_var); + dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); + break; + } + } else { /* find the corresponding closure variable */ if (var_idx & ARGUMENT_VAR_OFFSET) { fd->args[var_idx - ARGUMENT_VAR_OFFSET].is_captured = 1; idx = get_closure_var(ctx, s, fd, - TRUE, var_idx - ARGUMENT_VAR_OFFSET, + JS_CLOSURE_ARG, var_idx - ARGUMENT_VAR_OFFSET, var_name, FALSE, FALSE, JS_VAR_NORMAL); } else { fd->vars[var_idx].is_captured = 1; idx = get_closure_var(ctx, s, fd, - FALSE, var_idx, + JS_CLOSURE_LOCAL, var_idx, var_name, fd->vars[var_idx].is_const, fd->vars[var_idx].is_lexical, @@ -31382,15 +32579,22 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, dbuf_put_u16(bc, idx); } break; + case OP_scope_put_var: + if (s->closure_var[idx].var_kind == JS_VAR_FUNCTION_NAME) { + /* in non strict mode, modifying the function name is ignored */ + dbuf_putc(bc, OP_drop); + goto done; + } + goto closure_scope_var; case OP_scope_get_ref: /* XXX: should create a dummy object with a named slot that is a reference to the closure variable */ dbuf_putc(bc, OP_undefined); - /* fall thru */ + goto closure_scope_var; case OP_scope_get_var_undef: case OP_scope_get_var: - case OP_scope_put_var: case OP_scope_put_var_init: + closure_scope_var: is_put = (op == OP_scope_put_var || op == OP_scope_put_var_init); if (is_put) { @@ -31424,40 +32628,6 @@ static int resolve_scope_var(JSContext *ctx, JSFunctionDef *s, } } - /* global variable access */ - - switch (op) { - case OP_scope_make_ref: - if (label_done == -1 && can_opt_put_global_ref_value(bc_buf, ls->pos)) { - pos_next = optimize_scope_make_global_ref(ctx, s, bc, bc_buf, ls, - pos_next, var_name); - } else { - dbuf_putc(bc, OP_make_var_ref); - dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - } - break; - case OP_scope_get_ref: - /* XXX: should create a dummy object with a named slot that is - a reference to the global variable */ - dbuf_putc(bc, OP_undefined); - dbuf_putc(bc, OP_get_var); - dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - break; - case OP_scope_get_var_undef: - case OP_scope_get_var: - case OP_scope_put_var: - dbuf_putc(bc, OP_get_var_undef + (op - OP_scope_get_var_undef)); - dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - break; - case OP_scope_put_var_init: - dbuf_putc(bc, OP_put_var_init); - dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - break; - case OP_scope_delete_var: - dbuf_putc(bc, OP_delete_var); - dbuf_put_u32(bc, JS_DupAtom(ctx, var_name)); - break; - } done: if (label_done >= 0) { dbuf_putc(bc, OP_label); @@ -31509,7 +32679,7 @@ static int resolve_scope_private_field1(JSContext *ctx, if (idx >= 0) { var_kind = fd->vars[idx].var_kind; if (is_ref) { - idx = get_closure_var(ctx, s, fd, FALSE, idx, var_name, + idx = get_closure_var(ctx, s, fd, JS_CLOSURE_LOCAL, idx, var_name, TRUE, TRUE, JS_VAR_NORMAL); if (idx < 0) return -1; @@ -31526,12 +32696,12 @@ static int resolve_scope_private_field1(JSContext *ctx, var_kind = cv->var_kind; is_ref = TRUE; if (fd != s) { - idx = get_closure_var2(ctx, s, fd, - FALSE, - cv->is_arg, idx, - cv->var_name, cv->is_const, - cv->is_lexical, - cv->var_kind); + idx = get_closure_var(ctx, s, fd, + JS_CLOSURE_REF, + idx, + cv->var_name, cv->is_const, + cv->is_lexical, + cv->var_kind); if (idx < 0) return -1; } @@ -31760,7 +32930,7 @@ static void add_eval_variables(JSContext *ctx, JSFunctionDef *s) while (scope_idx >= 0) { vd = &fd->vars[scope_idx]; vd->is_captured = 1; - get_closure_var(ctx, s, fd, FALSE, scope_idx, + get_closure_var(ctx, s, fd, JS_CLOSURE_LOCAL, scope_idx, vd->var_name, vd->is_const, vd->is_lexical, vd->var_kind); scope_idx = vd->scope_next; } @@ -31772,7 +32942,7 @@ static void add_eval_variables(JSContext *ctx, JSFunctionDef *s) vd = &fd->args[i]; if (vd->var_name != JS_ATOM_NULL) { get_closure_var(ctx, s, fd, - TRUE, i, vd->var_name, FALSE, + JS_CLOSURE_ARG, i, vd->var_name, FALSE, vd->is_lexical, JS_VAR_NORMAL); } } @@ -31783,7 +32953,7 @@ static void add_eval_variables(JSContext *ctx, JSFunctionDef *s) vd->var_name != JS_ATOM__ret_ && vd->var_name != JS_ATOM_NULL) { get_closure_var(ctx, s, fd, - FALSE, i, vd->var_name, FALSE, + JS_CLOSURE_LOCAL, i, vd->var_name, FALSE, vd->is_lexical, JS_VAR_NORMAL); } } @@ -31793,7 +32963,7 @@ static void add_eval_variables(JSContext *ctx, JSFunctionDef *s) /* do not close top level last result */ if (vd->scope_level == 0 && is_var_in_arg_scope(vd)) { get_closure_var(ctx, s, fd, - FALSE, i, vd->var_name, FALSE, + JS_CLOSURE_LOCAL, i, vd->var_name, FALSE, vd->is_lexical, JS_VAR_NORMAL); } } @@ -31801,13 +32971,19 @@ static void add_eval_variables(JSContext *ctx, JSFunctionDef *s) if (fd->is_eval) { int idx; /* add direct eval variables (we are necessarily at the - top level) */ + top level). */ for (idx = 0; idx < fd->closure_var_count; idx++) { JSClosureVar *cv = &fd->closure_var[idx]; - get_closure_var2(ctx, s, fd, - FALSE, cv->is_arg, - idx, cv->var_name, cv->is_const, - cv->is_lexical, cv->var_kind); + /* Global variables are removed but module + definitions are kept. */ + if (cv->closure_type != JS_CLOSURE_GLOBAL_REF && + cv->closure_type != JS_CLOSURE_GLOBAL_DECL && + cv->closure_type != JS_CLOSURE_GLOBAL) { + get_closure_var(ctx, s, fd, + JS_CLOSURE_REF, + idx, cv->var_name, cv->is_const, + cv->is_lexical, cv->var_kind); + } } } } @@ -31816,8 +32992,7 @@ static void add_eval_variables(JSContext *ctx, JSFunctionDef *s) static void set_closure_from_var(JSContext *ctx, JSClosureVar *cv, JSVarDef *vd, int var_idx) { - cv->is_local = TRUE; - cv->is_arg = FALSE; + cv->closure_type = JS_CLOSURE_LOCAL; cv->is_const = vd->is_const; cv->is_lexical = vd->is_lexical; cv->var_kind = vd->var_kind; @@ -31858,8 +33033,7 @@ static __exception int add_closure_variables(JSContext *ctx, JSFunctionDef *s, for(i = 0; i < b->arg_count; i++) { JSClosureVar *cv = &s->closure_var[s->closure_var_count++]; vd = &b->vardefs[i]; - cv->is_local = TRUE; - cv->is_arg = TRUE; + cv->closure_type = JS_CLOSURE_ARG; cv->is_const = FALSE; cv->is_lexical = FALSE; cv->var_kind = JS_VAR_NORMAL; @@ -31886,9 +33060,24 @@ static __exception int add_closure_variables(JSContext *ctx, JSFunctionDef *s, } for(i = 0; i < b->closure_var_count; i++) { JSClosureVar *cv0 = &b->closure_var[i]; - JSClosureVar *cv = &s->closure_var[s->closure_var_count++]; - cv->is_local = FALSE; - cv->is_arg = cv0->is_arg; + JSClosureVar *cv; + + switch(cv0->closure_type) { + case JS_CLOSURE_LOCAL: + case JS_CLOSURE_ARG: + case JS_CLOSURE_REF: + case JS_CLOSURE_MODULE_DECL: + case JS_CLOSURE_MODULE_IMPORT: + break; + case JS_CLOSURE_GLOBAL_REF: + case JS_CLOSURE_GLOBAL_DECL: + case JS_CLOSURE_GLOBAL: + continue; /* not necessary to add global variables */ + default: + abort(); + } + cv = &s->closure_var[s->closure_var_count++]; + cv->closure_type = JS_CLOSURE_REF; cv->is_const = cv0->is_const; cv->is_lexical = cv0->is_lexical; cv->var_kind = cv0->var_kind; @@ -32066,8 +33255,11 @@ static void instantiate_hoisted_definitions(JSContext *ctx, JSFunctionDef *s, Dy evaluating the module so that the exported functions are visible if there are cyclic module references */ if (s->module) { - label_next = new_label_fd(s, -1); - + label_next = new_label_fd(s); + if (label_next < 0) { + dbuf_set_error(bc); + return; + } /* if 'this' is true, initialize the global variables and return */ dbuf_putc(bc, OP_push_this); dbuf_putc(bc, OP_if_false); @@ -32078,9 +33270,10 @@ static void instantiate_hoisted_definitions(JSContext *ctx, JSFunctionDef *s, Dy /* add the global variables (only happens if s->is_global_var is true) */ + /* XXX: inefficient, add a closure index in JSGlobalVar */ for(i = 0; i < s->global_var_count; i++) { JSGlobalVar *hf = &s->global_vars[i]; - int has_closure = 0; + BOOL has_var_obj = FALSE; BOOL force_init = hf->force_init; /* we are in an eval, so the closure contains all the enclosing variables */ @@ -32089,46 +33282,20 @@ static void instantiate_hoisted_definitions(JSContext *ctx, JSFunctionDef *s, Dy for(idx = 0; idx < s->closure_var_count; idx++) { JSClosureVar *cv = &s->closure_var[idx]; if (cv->var_name == hf->var_name) { - has_closure = 2; force_init = FALSE; - break; + goto closure_found; } if (cv->var_name == JS_ATOM__var_ || cv->var_name == JS_ATOM__arg_var_) { dbuf_putc(bc, OP_get_var_ref); dbuf_put_u16(bc, idx); - has_closure = 1; + has_var_obj = TRUE; force_init = TRUE; - break; - } - } - if (!has_closure) { - int flags; - - flags = 0; - if (s->eval_type != JS_EVAL_TYPE_GLOBAL) - flags |= JS_PROP_CONFIGURABLE; - if (hf->cpool_idx >= 0 && !hf->is_lexical) { - /* global function definitions need a specific handling */ - dbuf_putc(bc, OP_fclosure); - dbuf_put_u32(bc, hf->cpool_idx); - - dbuf_putc(bc, OP_define_func); - dbuf_put_u32(bc, JS_DupAtom(ctx, hf->var_name)); - dbuf_putc(bc, flags); - - goto done_global_var; - } else { - if (hf->is_lexical) { - flags |= DEFINE_GLOBAL_LEX_VAR; - if (!hf->is_const) - flags |= JS_PROP_WRITABLE; - } - dbuf_putc(bc, OP_define_var); - dbuf_put_u32(bc, JS_DupAtom(ctx, hf->var_name)); - dbuf_putc(bc, flags); + goto closure_found; } } + abort(); + closure_found: if (hf->cpool_idx >= 0 || force_init) { if (hf->cpool_idx >= 0) { dbuf_putc(bc, OP_fclosure); @@ -32141,20 +33308,15 @@ static void instantiate_hoisted_definitions(JSContext *ctx, JSFunctionDef *s, Dy } else { dbuf_putc(bc, OP_undefined); } - if (has_closure == 2) { + if (!has_var_obj) { dbuf_putc(bc, OP_put_var_ref); dbuf_put_u16(bc, idx); - } else if (has_closure == 1) { + } else { dbuf_putc(bc, OP_define_field); dbuf_put_u32(bc, JS_DupAtom(ctx, hf->var_name)); dbuf_putc(bc, OP_drop); - } else { - /* XXX: Check if variable is writable and enumerable */ - dbuf_putc(bc, OP_put_var); - dbuf_put_u32(bc, JS_DupAtom(ctx, hf->var_name)); } } - done_global_var: JS_FreeAtom(ctx, hf->var_name); } @@ -32249,7 +33411,7 @@ static int get_label_pos(JSFunctionDef *s, int label) variables when necessary */ static __exception int resolve_variables(JSContext *ctx, JSFunctionDef *s) { - int pos, pos_next, bc_len, op, len, i, idx, line_num; + int pos, pos_next, bc_len, op, len, line_num, i, idx; uint8_t *bc_buf; JSAtom var_name; DynBuf bc_out; @@ -32258,17 +33420,23 @@ static __exception int resolve_variables(JSContext *ctx, JSFunctionDef *s) cc.bc_buf = bc_buf = s->byte_code.buf; cc.bc_len = bc_len = s->byte_code.size; - js_dbuf_init(ctx, &bc_out); + js_dbuf_bytecode_init(ctx, &bc_out); /* first pass for runtime checks (must be done before the variables are created) */ + /* XXX: inefficient */ for(i = 0; i < s->global_var_count; i++) { JSGlobalVar *hf = &s->global_vars[i]; - int flags; /* check if global variable (XXX: simplify) */ for(idx = 0; idx < s->closure_var_count; idx++) { JSClosureVar *cv = &s->closure_var[idx]; + if (cv->closure_type == JS_CLOSURE_GLOBAL_REF || + cv->closure_type == JS_CLOSURE_GLOBAL_DECL || + cv->closure_type == JS_CLOSURE_GLOBAL || + cv->closure_type == JS_CLOSURE_MODULE_DECL || + cv->closure_type == JS_CLOSURE_MODULE_IMPORT) + goto next; /* don't look at global variables (they are at the end) */ if (cv->var_name == hf->var_name) { if (s->eval_type == JS_EVAL_TYPE_DIRECT && cv->is_lexical) { @@ -32287,15 +33455,6 @@ static __exception int resolve_variables(JSContext *ctx, JSFunctionDef *s) cv->var_name == JS_ATOM__arg_var_) goto next; } - - dbuf_putc(&bc_out, OP_check_define_var); - dbuf_put_u32(&bc_out, JS_DupAtom(ctx, hf->var_name)); - flags = 0; - if (hf->is_lexical) - flags |= DEFINE_GLOBAL_LEX_VAR; - if (hf->cpool_idx >= 0) - flags |= DEFINE_GLOBAL_FUNC_VAR; - dbuf_putc(&bc_out, flags); next: ; } @@ -32878,7 +34037,7 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) cc.bc_buf = bc_buf = s->byte_code.buf; cc.bc_len = bc_len = s->byte_code.size; - js_dbuf_init(ctx, &bc_out); + js_dbuf_bytecode_init(ctx, &bc_out); #if SHORT_OPCODES if (s->jump_size) { @@ -33414,12 +34573,11 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) if (OPTIMIZE) { /* Transformation: insert2 put_field(a) drop -> put_field(a) - insert2 put_var_strict(a) drop -> put_var_strict(a) */ - if (code_match(&cc, pos_next, M2(OP_put_field, OP_put_var_strict), OP_drop, -1)) { + if (code_match(&cc, pos_next, OP_put_field, OP_drop, -1)) { if (cc.line_num >= 0) line_num = cc.line_num; add_pc2line_info(s, bc_out.size, line_num); - dbuf_putc(&bc_out, cc.op); + dbuf_putc(&bc_out, OP_put_field); dbuf_put_u32(&bc_out, cc.atom); pos_next = cc.pos; break; @@ -33565,7 +34723,6 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) /* transformation: post_inc put_x drop -> inc put_x post_inc perm3 put_field drop -> inc put_field - post_inc perm3 put_var_strict drop -> inc put_var_strict post_inc perm4 put_array_el drop -> inc put_array_el */ int op1, idx; @@ -33584,11 +34741,11 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s) put_short_code(&bc_out, op1, idx); break; } - if (code_match(&cc, pos_next, OP_perm3, M2(OP_put_field, OP_put_var_strict), OP_drop, -1)) { + if (code_match(&cc, pos_next, OP_perm3, OP_put_field, OP_drop, -1)) { if (cc.line_num >= 0) line_num = cc.line_num; add_pc2line_info(s, bc_out.size, line_num); dbuf_putc(&bc_out, OP_dec + (op - OP_post_dec)); - dbuf_putc(&bc_out, cc.op); + dbuf_putc(&bc_out, OP_put_field); dbuf_put_u32(&bc_out, cc.atom); pos_next = cc.pos; break; @@ -34013,35 +35170,68 @@ static __exception int compute_stack_size(JSContext *ctx, return -1; } -static int add_module_variables(JSContext *ctx, JSFunctionDef *fd) +static int add_global_variables(JSContext *ctx, JSFunctionDef *fd) { int i, idx; JSModuleDef *m = fd->module; JSExportEntry *me; JSGlobalVar *hf; + BOOL need_global_closures; + + /* Script: add the defined global variables. In the non strict + direct eval not in global scope, the global variables are + created in the enclosing scope so they are not created as + variable references. - /* The imported global variables were added as closure variables - in js_parse_import(). We add here the module global - variables. */ - - for(i = 0; i < fd->global_var_count; i++) { - hf = &fd->global_vars[i]; - if (add_closure_var(ctx, fd, TRUE, FALSE, i, hf->var_name, hf->is_const, - hf->is_lexical, FALSE) < 0) - return -1; + In modules, the imported global variables were added as closure + global variables in js_parse_import(). + */ + need_global_closures = TRUE; + if (fd->eval_type == JS_EVAL_TYPE_DIRECT && !(fd->js_mode & JS_MODE_STRICT)) { + /* XXX: add a flag ? */ + for(idx = 0; idx < fd->closure_var_count; idx++) { + JSClosureVar *cv = &fd->closure_var[idx]; + if (cv->var_name == JS_ATOM__var_ || + cv->var_name == JS_ATOM__arg_var_) { + need_global_closures = FALSE; + break; + } + } } - /* resolve the variable names of the local exports */ - for(i = 0; i < m->export_entries_count; i++) { - me = &m->export_entries[i]; - if (me->export_type == JS_EXPORT_TYPE_LOCAL) { - idx = find_closure_var(ctx, fd, me->local_name); - if (idx < 0) { - JS_ThrowSyntaxErrorAtom(ctx, "exported variable '%s' does not exist", - me->local_name); + if (need_global_closures) { + JSClosureTypeEnum closure_type; + if (fd->module) + closure_type = JS_CLOSURE_MODULE_DECL; + else + closure_type = JS_CLOSURE_GLOBAL_DECL; + for(i = 0; i < fd->global_var_count; i++) { + JSVarKindEnum var_kind; + hf = &fd->global_vars[i]; + if (hf->cpool_idx >= 0 && !hf->is_lexical) { + var_kind = JS_VAR_GLOBAL_FUNCTION_DECL; + } else { + var_kind = JS_VAR_NORMAL; + } + if (add_closure_var(ctx, fd, closure_type, i, hf->var_name, hf->is_const, + hf->is_lexical, var_kind) < 0) return -1; + } + } + + if (fd->module) { + /* resolve the variable names of the local exports */ + for(i = 0; i < m->export_entries_count; i++) { + me = &m->export_entries[i]; + if (me->export_type == JS_EXPORT_TYPE_LOCAL) { + idx = find_closure_var(ctx, fd, me->local_name); + if (idx < 0) { + JS_ThrowSyntaxErrorAtom(ctx, "exported variable '%s' does not exist", + me->local_name); + return -1; + } + me->u.local.var_idx = idx; } - me->u.local.var_idx = idx; } } return 0; @@ -34093,10 +35283,10 @@ static JSValue js_create_function(JSContext *ctx, JSFunctionDef *fd) add_eval_variables(ctx, fd); /* add the module global variables in the closure */ - if (fd->module) { - if (add_module_variables(ctx, fd)) + if (fd->is_eval) { + if (add_global_variables(ctx, fd)) goto fail; - } + } /* first create all the child functions */ list_for_each_safe(el, el1, &fd->child_list) { @@ -34286,7 +35476,8 @@ static void free_function_bytecode(JSRuntime *rt, JSFunctionBytecode *b) JS_AtomGetStrRT(rt, buf, sizeof(buf), b->func_name)); } #endif - free_bytecode_atoms(rt, b->byte_code_buf, b->byte_code_len, TRUE); + if (b->byte_code_buf) + free_bytecode_atoms(rt, b->byte_code_buf, b->byte_code_len, TRUE); if (b->vardefs) { for(i = 0; i < b->arg_count + b->var_count; i++) { @@ -34878,7 +36069,7 @@ static __exception int js_parse_function_decl2(JSParseState *s, push_scope(s); /* enter body scope */ fd->body_scope = fd->scope_level; - if (s->token.val == TOK_ARROW) { + if (s->token.val == TOK_ARROW && func_type == JS_PARSE_FUNC_ARROW) { if (next_token(s)) goto fail; @@ -35148,7 +36339,9 @@ static JSValue JS_EvalFunctionInternal(JSContext *ctx, JSValue fun_obj, tag = JS_VALUE_GET_TAG(fun_obj); if (tag == JS_TAG_FUNCTION_BYTECODE) { - fun_obj = js_closure(ctx, fun_obj, var_refs, sf); + fun_obj = js_closure(ctx, fun_obj, var_refs, sf, TRUE); + if (JS_IsException(fun_obj)) + return JS_EXCEPTION; ret_val = JS_CallFree(ctx, fun_obj, this_obj, 0, NULL); } else if (tag == JS_TAG_MODULE) { JSModuleDef *m; @@ -35288,7 +36481,7 @@ static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, fail1: /* XXX: should free all the unresolved dependencies */ if (m) - js_free_module_def(ctx, m); + JS_FreeValue(ctx, JS_MKPTR(JS_TAG_MODULE, m)); return JS_EXCEPTION; } @@ -35872,13 +37065,12 @@ static int JS_WriteFunctionTag(BCWriterState *s, JSValueConst obj) bc_put_atom(s, cv->var_name); bc_put_leb128(s, cv->var_idx); flags = idx = 0; - bc_set_flags(&flags, &idx, cv->is_local, 1); - bc_set_flags(&flags, &idx, cv->is_arg, 1); + bc_set_flags(&flags, &idx, cv->closure_type, 3); bc_set_flags(&flags, &idx, cv->is_const, 1); bc_set_flags(&flags, &idx, cv->is_lexical, 1); bc_set_flags(&flags, &idx, cv->var_kind, 4); - assert(idx <= 8); - bc_put_u8(s, flags); + assert(idx <= 16); + bc_put_u16(s, flags); } if (JS_WriteFunctionBytecode(s, b->byte_code_buf, b->byte_code_len)) @@ -35917,6 +37109,8 @@ static int JS_WriteModule(BCWriterState *s, JSValueConst obj) for(i = 0; i < m->req_module_entries_count; i++) { JSReqModuleEntry *rme = &m->req_module_entries[i]; bc_put_atom(s, rme->module_name); + if (JS_WriteObjectRec(s, rme->attributes)) + goto fail; } bc_put_leb128(s, m->export_entries_count); @@ -36062,6 +37256,7 @@ static int JS_WriteArrayBuffer(BCWriterState *s, JSValueConst obj) } bc_put_u8(s, BC_TAG_ARRAY_BUFFER); bc_put_leb128(s, abuf->byte_length); + bc_put_leb128(s, abuf->max_byte_length); dbuf_put(&s->dbuf, abuf->data, abuf->byte_length); return 0; } @@ -36073,6 +37268,7 @@ static int JS_WriteSharedArrayBuffer(BCWriterState *s, JSValueConst obj) assert(!abuf->detached); /* SharedArrayBuffer are never detached */ bc_put_u8(s, BC_TAG_SHARED_ARRAY_BUFFER); bc_put_leb128(s, abuf->byte_length); + bc_put_leb128(s, abuf->max_byte_length); bc_put_u64(s, (uintptr_t)abuf->data); if (js_resize_array(s->ctx, (void **)&s->sab_tab, sizeof(s->sab_tab[0]), &s->sab_tab_size, s->sab_tab_len + 1)) @@ -36244,7 +37440,7 @@ static int JS_WriteObjectAtoms(BCWriterState *s) /* XXX: could just append dbuf1 data, but it uses more memory if dbuf1 is larger than dbuf */ atoms_size = s->dbuf.size; - if (dbuf_realloc(&dbuf1, dbuf1.size + atoms_size)) + if (dbuf_claim(&dbuf1, atoms_size)) goto fail; memmove(dbuf1.buf + atoms_size, dbuf1.buf, dbuf1.size); memcpy(dbuf1.buf, s->dbuf.buf, atoms_size); @@ -36822,14 +38018,13 @@ static JSValue JS_ReadFunctionTag(BCReaderState *s) if (bc_get_leb128_int(s, &var_idx)) goto fail; cv->var_idx = var_idx; - if (bc_get_u8(s, &v8)) + if (bc_get_u16(s, &v16)) goto fail; idx = 0; - cv->is_local = bc_get_flags(v8, &idx, 1); - cv->is_arg = bc_get_flags(v8, &idx, 1); - cv->is_const = bc_get_flags(v8, &idx, 1); - cv->is_lexical = bc_get_flags(v8, &idx, 1); - cv->var_kind = bc_get_flags(v8, &idx, 4); + cv->closure_type = bc_get_flags(v16, &idx, 3); + cv->is_const = bc_get_flags(v16, &idx, 1); + cv->is_lexical = bc_get_flags(v16, &idx, 1); + cv->var_kind = bc_get_flags(v16, &idx, 4); #ifdef DUMP_READ_OBJECT bc_read_trace(s, "name: "); print_atom(s->ctx, cv->var_name); printf("\n"); #endif @@ -36916,8 +38111,13 @@ static JSValue JS_ReadModule(BCReaderState *s) goto fail; for(i = 0; i < m->req_module_entries_count; i++) { JSReqModuleEntry *rme = &m->req_module_entries[i]; + JSValue val; if (bc_get_atom(s, &rme->module_name)) goto fail; + val = JS_ReadObjectRec(s); + if (JS_IsException(val)) + goto fail; + rme->attributes = val; } } @@ -36993,7 +38193,7 @@ static JSValue JS_ReadModule(BCReaderState *s) return obj; fail: if (m) { - js_free_module_def(ctx, m); + JS_FreeValue(ctx, JS_MKPTR(JS_TAG_MODULE, m)); } return JS_EXCEPTION; } @@ -37129,16 +38329,31 @@ static JSValue JS_ReadTypedArray(BCReaderState *s) static JSValue JS_ReadArrayBuffer(BCReaderState *s) { JSContext *ctx = s->ctx; - uint32_t byte_length; + uint32_t byte_length, max_byte_length; + uint64_t max_byte_length_u64, *pmax_byte_length = NULL; JSValue obj; if (bc_get_leb128(s, &byte_length)) return JS_EXCEPTION; + if (bc_get_leb128(s, &max_byte_length)) + return JS_EXCEPTION; + if (max_byte_length < byte_length) + return JS_ThrowTypeError(ctx, "invalid array buffer"); + if (max_byte_length != UINT32_MAX) { + max_byte_length_u64 = max_byte_length; + pmax_byte_length = &max_byte_length_u64; + } if (unlikely(s->buf_end - s->ptr < byte_length)) { bc_read_error_end(s); return JS_EXCEPTION; } - obj = JS_NewArrayBufferCopy(ctx, s->ptr, byte_length); + // makes a copy of the input + obj = js_array_buffer_constructor3(ctx, JS_UNDEFINED, + byte_length, pmax_byte_length, + JS_CLASS_ARRAY_BUFFER, + (uint8_t*)s->ptr, + js_array_buffer_free, NULL, + /*alloc_flag*/TRUE); if (JS_IsException(obj)) goto fail; if (BC_add_object_ref(s, obj)) @@ -37153,18 +38368,28 @@ static JSValue JS_ReadArrayBuffer(BCReaderState *s) static JSValue JS_ReadSharedArrayBuffer(BCReaderState *s) { JSContext *ctx = s->ctx; - uint32_t byte_length; + uint32_t byte_length, max_byte_length; + uint64_t max_byte_length_u64, *pmax_byte_length = NULL; uint8_t *data_ptr; JSValue obj; uint64_t u64; if (bc_get_leb128(s, &byte_length)) return JS_EXCEPTION; + if (bc_get_leb128(s, &max_byte_length)) + return JS_EXCEPTION; + if (max_byte_length < byte_length) + return JS_ThrowTypeError(ctx, "invalid array buffer"); + if (max_byte_length != UINT32_MAX) { + max_byte_length_u64 = max_byte_length; + pmax_byte_length = &max_byte_length_u64; + } if (bc_get_u64(s, &u64)) return JS_EXCEPTION; data_ptr = (uint8_t *)(uintptr_t)u64; /* the SharedArrayBuffer is cloned */ - obj = js_array_buffer_constructor3(ctx, JS_UNDEFINED, byte_length, + obj = js_array_buffer_constructor3(ctx, JS_UNDEFINED, + byte_length, pmax_byte_length, JS_CLASS_SHARED_ARRAY_BUFFER, data_ptr, NULL, NULL, FALSE); @@ -37468,11 +38693,25 @@ static JSAtom find_atom(JSContext *ctx, const char *name) return atom; } +static JSValue JS_NewObjectProtoList(JSContext *ctx, JSValueConst proto, + const JSCFunctionListEntry *fields, int n_fields) +{ + JSValue obj; + obj = JS_NewObjectProtoClassAlloc(ctx, proto, JS_CLASS_OBJECT, n_fields); + if (JS_IsException(obj)) + return obj; + if (JS_SetPropertyFunctionList(ctx, obj, fields, n_fields)) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + return obj; +} + static JSValue JS_InstantiateFunctionListItem2(JSContext *ctx, JSObject *p, JSAtom atom, void *opaque) { const JSCFunctionListEntry *e = opaque; - JSValue val; + JSValue val, proto; switch(e->def_type) { case JS_DEF_CFUNC: @@ -37483,8 +38722,13 @@ static JSValue JS_InstantiateFunctionListItem2(JSContext *ctx, JSObject *p, val = JS_NewAtomString(ctx, e->u.str); break; case JS_DEF_OBJECT: - val = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, val, e->u.prop_list.tab, e->u.prop_list.len); + /* XXX: could add a flag */ + if (atom == JS_ATOM_Symbol_unscopables) + proto = JS_NULL; + else + proto = ctx->class_proto[JS_CLASS_OBJECT]; + val = JS_NewObjectProtoList(ctx, proto, + e->u.prop_list.tab, e->u.prop_list.len); break; default: abort(); @@ -37573,6 +38817,12 @@ static int JS_InstantiateFunctionListItem(JSContext *ctx, JSValueConst obj, case JS_DEF_PROP_UNDEFINED: val = JS_UNDEFINED; break; + case JS_DEF_PROP_ATOM: + val = JS_AtomToValue(ctx, e->u.i32); + break; + case JS_DEF_PROP_BOOL: + val = JS_NewBool(ctx, e->u.i32); + break; case JS_DEF_PROP_STRING: case JS_DEF_OBJECT: JS_DefineAutoInitProperty(ctx, obj, atom, JS_AUTOINIT_ID_PROP, @@ -37585,17 +38835,22 @@ static int JS_InstantiateFunctionListItem(JSContext *ctx, JSValueConst obj, return 0; } -void JS_SetPropertyFunctionList(JSContext *ctx, JSValueConst obj, - const JSCFunctionListEntry *tab, int len) +int JS_SetPropertyFunctionList(JSContext *ctx, JSValueConst obj, + const JSCFunctionListEntry *tab, int len) { - int i; + int i, ret; for (i = 0; i < len; i++) { const JSCFunctionListEntry *e = &tab[i]; JSAtom atom = find_atom(ctx, e->name); - JS_InstantiateFunctionListItem(ctx, obj, atom, e); + if (atom == JS_ATOM_NULL) + return -1; + ret = JS_InstantiateFunctionListItem(ctx, obj, atom, e); JS_FreeAtom(ctx, atom); + if (ret) + return -1; } + return 0; } int JS_AddModuleExportList(JSContext *ctx, JSModuleDef *m, @@ -37635,8 +38890,8 @@ int JS_SetModuleExportList(JSContext *ctx, JSModuleDef *m, val = __JS_NewFloat64(ctx, e->u.f64); break; case JS_DEF_OBJECT: - val = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, val, e->u.prop_list.tab, e->u.prop_list.len); + val = JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_OBJECT], + e->u.prop_list.tab, e->u.prop_list.len); break; default: abort(); @@ -37648,57 +38903,107 @@ int JS_SetModuleExportList(JSContext *ctx, JSModuleDef *m, } /* Note: 'func_obj' is not necessarily a constructor */ -static void JS_SetConstructor2(JSContext *ctx, - JSValueConst func_obj, - JSValueConst proto, - int proto_flags, int ctor_flags) -{ - JS_DefinePropertyValue(ctx, func_obj, JS_ATOM_prototype, - JS_DupValue(ctx, proto), proto_flags); - JS_DefinePropertyValue(ctx, proto, JS_ATOM_constructor, - JS_DupValue(ctx, func_obj), - ctor_flags); +static int JS_SetConstructor2(JSContext *ctx, + JSValueConst func_obj, + JSValueConst proto, + int proto_flags, int ctor_flags) +{ + if (JS_DefinePropertyValue(ctx, func_obj, JS_ATOM_prototype, + JS_DupValue(ctx, proto), proto_flags) < 0) + return -1; + if (JS_DefinePropertyValue(ctx, proto, JS_ATOM_constructor, + JS_DupValue(ctx, func_obj), + ctor_flags) < 0) + return -1; set_cycle_flag(ctx, func_obj); set_cycle_flag(ctx, proto); + return 0; } -void JS_SetConstructor(JSContext *ctx, JSValueConst func_obj, - JSValueConst proto) +/* return 0 if OK, -1 if exception */ +int JS_SetConstructor(JSContext *ctx, JSValueConst func_obj, + JSValueConst proto) { - JS_SetConstructor2(ctx, func_obj, proto, - 0, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + return JS_SetConstructor2(ctx, func_obj, proto, + 0, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); } -static void JS_NewGlobalCConstructor2(JSContext *ctx, - JSValue func_obj, - const char *name, - JSValueConst proto) -{ - JS_DefinePropertyValueStr(ctx, ctx->global_obj, name, - JS_DupValue(ctx, func_obj), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - JS_SetConstructor(ctx, func_obj, proto); - JS_FreeValue(ctx, func_obj); -} +#define JS_NEW_CTOR_NO_GLOBAL (1 << 0) /* don't create a global binding */ +#define JS_NEW_CTOR_PROTO_CLASS (1 << 1) /* the prototype class is 'class_id' instead of JS_CLASS_OBJECT */ +#define JS_NEW_CTOR_PROTO_EXIST (1 << 2) /* the prototype is already defined */ +#define JS_NEW_CTOR_READONLY (1 << 3) /* read-only constructor field */ -static JSValueConst JS_NewGlobalCConstructor(JSContext *ctx, const char *name, - JSCFunction *func, int length, - JSValueConst proto) +/* Return the constructor and. Define it as a global variable unless + JS_NEW_CTOR_NO_GLOBAL is set. The new class inherit from + parent_ctor if it is not JS_UNDEFINED. if class_id is != -1, + class_proto[class_id] is set. */ +static JSValue JS_NewCConstructor(JSContext *ctx, int class_id, const char *name, + JSCFunction *func, int length, JSCFunctionEnum cproto, int magic, + JSValueConst parent_ctor, + const JSCFunctionListEntry *ctor_fields, int n_ctor_fields, + const JSCFunctionListEntry *proto_fields, int n_proto_fields, + int flags) { - JSValue func_obj; - func_obj = JS_NewCFunction2(ctx, func, name, length, JS_CFUNC_constructor_or_func, 0); - JS_NewGlobalCConstructor2(ctx, func_obj, name, proto); - return func_obj; -} + JSValue ctor = JS_UNDEFINED, proto, parent_proto; + int proto_class_id, proto_flags, ctor_flags; -static JSValueConst JS_NewGlobalCConstructorOnly(JSContext *ctx, const char *name, - JSCFunction *func, int length, - JSValueConst proto) -{ - JSValue func_obj; - func_obj = JS_NewCFunction2(ctx, func, name, length, JS_CFUNC_constructor, 0); - JS_NewGlobalCConstructor2(ctx, func_obj, name, proto); - return func_obj; + proto_flags = 0; + if (flags & JS_NEW_CTOR_READONLY) { + ctor_flags = JS_PROP_CONFIGURABLE; + } else { + ctor_flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE; + } + + if (JS_IsUndefined(parent_ctor)) { + parent_proto = JS_DupValue(ctx, ctx->class_proto[JS_CLASS_OBJECT]); + parent_ctor = ctx->function_proto; + } else { + parent_proto = JS_GetProperty(ctx, parent_ctor, JS_ATOM_prototype); + if (JS_IsException(parent_proto)) + return JS_EXCEPTION; + } + + if (flags & JS_NEW_CTOR_PROTO_EXIST) { + proto = JS_DupValue(ctx, ctx->class_proto[class_id]); + } else { + if (flags & JS_NEW_CTOR_PROTO_CLASS) + proto_class_id = class_id; + else + proto_class_id = JS_CLASS_OBJECT; + /* one additional field: constructor */ + proto = JS_NewObjectProtoClassAlloc(ctx, parent_proto, proto_class_id, + n_proto_fields + 1); + if (JS_IsException(proto)) + goto fail; + if (class_id >= 0) + ctx->class_proto[class_id] = JS_DupValue(ctx, proto); + } + if (JS_SetPropertyFunctionList(ctx, proto, proto_fields, n_proto_fields)) + goto fail; + + /* additional fields: name, length, prototype */ + ctor = JS_NewCFunction3(ctx, func, name, length, cproto, magic, parent_ctor, + n_ctor_fields + 3); + if (JS_IsException(ctor)) + goto fail; + if (JS_SetPropertyFunctionList(ctx, ctor, ctor_fields, n_ctor_fields)) + goto fail; + if (!(flags & JS_NEW_CTOR_NO_GLOBAL)) { + if (JS_DefinePropertyValueStr(ctx, ctx->global_obj, name, + JS_DupValue(ctx, ctor), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0) + goto fail; + } + JS_SetConstructor2(ctx, ctor, proto, proto_flags, ctor_flags); + + JS_FreeValue(ctx, proto); + JS_FreeValue(ctx, parent_proto); + return ctor; + fail: + JS_FreeValue(ctx, proto); + JS_FreeValue(ctx, parent_proto); + JS_FreeValue(ctx, ctor); + return JS_EXCEPTION; } static JSValue js_global_eval(JSContext *ctx, JSValueConst this_val, @@ -37913,7 +39218,7 @@ static __exception int JS_ObjectDefineProperties(JSContext *ctx, ret = 0; exception: - js_free_prop_enum(ctx, atoms, len); + JS_FreePropertyEnum(ctx, atoms, len); JS_FreeValue(ctx, props); JS_FreeValue(ctx, desc); return ret; @@ -38184,12 +39489,12 @@ static JSValue js_object_getOwnPropertyDescriptors(JSContext *ctx, JSValueConst goto exception; } } - js_free_prop_enum(ctx, props, len); + JS_FreePropertyEnum(ctx, props, len); JS_FreeValue(ctx, obj); return r; exception: - js_free_prop_enum(ctx, props, len); + JS_FreePropertyEnum(ctx, props, len); JS_FreeValue(ctx, obj); JS_FreeValue(ctx, r); return JS_EXCEPTION; @@ -38269,7 +39574,7 @@ static JSValue JS_GetOwnPropertyNames2(JSContext *ctx, JSValueConst obj1, JS_FreeValue(ctx, r); r = JS_EXCEPTION; done: - js_free_prop_enum(ctx, atoms, len); + JS_FreePropertyEnum(ctx, atoms, len); JS_FreeValue(ctx, obj); return r; } @@ -38530,11 +39835,11 @@ static JSValue js_object_seal(JSContext *ctx, JSValueConst this_val, JS_UNDEFINED, JS_UNDEFINED, desc_flags) < 0) goto exception; } - js_free_prop_enum(ctx, props, len); + JS_FreePropertyEnum(ctx, props, len); return JS_DupValue(ctx, obj); exception: - js_free_prop_enum(ctx, props, len); + JS_FreePropertyEnum(ctx, props, len); return JS_EXCEPTION; } @@ -38576,11 +39881,11 @@ static JSValue js_object_isSealed(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; res ^= 1; done: - js_free_prop_enum(ctx, props, len); + JS_FreePropertyEnum(ctx, props, len); return JS_NewBool(ctx, res); exception: - js_free_prop_enum(ctx, props, len); + JS_FreePropertyEnum(ctx, props, len); return JS_EXCEPTION; } @@ -38650,107 +39955,12 @@ static JSValue js_object_fromEntries(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; } -#if 0 -/* Note: corresponds to ECMA spec: CreateDataPropertyOrThrow() */ -static JSValue js_object___setOwnProperty(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - int ret; - ret = JS_DefinePropertyValueValue(ctx, argv[0], JS_DupValue(ctx, argv[1]), - JS_DupValue(ctx, argv[2]), - JS_PROP_C_W_E | JS_PROP_THROW); - if (ret < 0) - return JS_EXCEPTION; - else - return JS_NewBool(ctx, ret); -} - -static JSValue js_object___toObject(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_ToObject(ctx, argv[0]); -} - -static JSValue js_object___toPrimitive(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - int hint = HINT_NONE; - - if (JS_VALUE_GET_TAG(argv[1]) == JS_TAG_INT) - hint = JS_VALUE_GET_INT(argv[1]); - - return JS_ToPrimitive(ctx, argv[0], hint); -} -#endif - -/* return an empty string if not an object */ -static JSValue js_object___getClass(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - JSAtom atom; - JSObject *p; - uint32_t tag; - int class_id; - - tag = JS_VALUE_GET_NORM_TAG(argv[0]); - if (tag == JS_TAG_OBJECT) { - p = JS_VALUE_GET_OBJ(argv[0]); - class_id = p->class_id; - if (class_id == JS_CLASS_PROXY && JS_IsFunction(ctx, argv[0])) - class_id = JS_CLASS_BYTECODE_FUNCTION; - atom = ctx->rt->class_array[class_id].class_name; - } else { - atom = JS_ATOM_empty_string; - } - return JS_AtomToString(ctx, atom); -} - static JSValue js_object_is(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { return JS_NewBool(ctx, js_same_value(ctx, argv[0], argv[1])); } -#if 0 -static JSValue js_object___getObjectData(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_GetObjectData(ctx, argv[0]); -} - -static JSValue js_object___setObjectData(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - if (JS_SetObjectData(ctx, argv[0], JS_DupValue(ctx, argv[1]))) - return JS_EXCEPTION; - return JS_DupValue(ctx, argv[1]); -} - -static JSValue js_object___toPropertyKey(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_ToPropertyKey(ctx, argv[0]); -} - -static JSValue js_object___isObject(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_NewBool(ctx, JS_IsObject(argv[0])); -} - -static JSValue js_object___isSameValueZero(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_NewBool(ctx, js_same_value_zero(ctx, argv[0], argv[1])); -} - -static JSValue js_object___isConstructor(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_NewBool(ctx, JS_IsConstructor(ctx, argv[0])); -} -#endif - static JSValue JS_SpeciesConstructor(JSContext *ctx, JSValueConst obj, JSValueConst defaultConstructor) { @@ -38774,20 +39984,13 @@ static JSValue JS_SpeciesConstructor(JSContext *ctx, JSValueConst obj, if (JS_IsUndefined(species) || JS_IsNull(species)) return JS_DupValue(ctx, defaultConstructor); if (!JS_IsConstructor(ctx, species)) { + JS_ThrowTypeErrorNotAConstructor(ctx, species); JS_FreeValue(ctx, species); - return JS_ThrowTypeError(ctx, "not a constructor"); + return JS_EXCEPTION; } return species; } -#if 0 -static JSValue js_object___speciesConstructor(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_SpeciesConstructor(ctx, argv[0], argv[1]); -} -#endif - static JSValue js_object_get___proto__(JSContext *ctx, JSValueConst this_val) { JSValue val, ret; @@ -38951,17 +40154,6 @@ static const JSCFunctionListEntry js_object_funcs[] = { JS_CFUNC_MAGIC_DEF("freeze", 1, js_object_seal, 1 ), JS_CFUNC_MAGIC_DEF("isSealed", 1, js_object_isSealed, 0 ), JS_CFUNC_MAGIC_DEF("isFrozen", 1, js_object_isSealed, 1 ), - JS_CFUNC_DEF("__getClass", 1, js_object___getClass ), - //JS_CFUNC_DEF("__isObject", 1, js_object___isObject ), - //JS_CFUNC_DEF("__isConstructor", 1, js_object___isConstructor ), - //JS_CFUNC_DEF("__toObject", 1, js_object___toObject ), - //JS_CFUNC_DEF("__setOwnProperty", 3, js_object___setOwnProperty ), - //JS_CFUNC_DEF("__toPrimitive", 2, js_object___toPrimitive ), - //JS_CFUNC_DEF("__toPropertyKey", 1, js_object___toPropertyKey ), - //JS_CFUNC_DEF("__speciesConstructor", 2, js_object___speciesConstructor ), - //JS_CFUNC_DEF("__isSameValueZero", 2, js_object___isSameValueZero ), - //JS_CFUNC_DEF("__getObjectData", 1, js_object___getObjectData ), - //JS_CFUNC_DEF("__setObjectData", 2, js_object___setObjectData ), JS_CFUNC_DEF("fromEntries", 1, js_object_fromEntries ), JS_CFUNC_DEF("hasOwn", 2, js_object_hasOwn ), }; @@ -39478,6 +40670,35 @@ static const JSCFunctionListEntry js_error_proto_funcs[] = { JS_PROP_STRING_DEF("message", "", JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE ), }; +/* 2 entries for each native error class */ +/* Note: we use an atom to avoid the autoinit definition which does + not work in get_prop_string() */ +static const JSCFunctionListEntry js_native_error_proto_funcs[] = { +#define DEF(name) \ + JS_PROP_ATOM_DEF("name", name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE ),\ + JS_PROP_STRING_DEF("message", "", JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE ), + + DEF(JS_ATOM_EvalError) + DEF(JS_ATOM_RangeError) + DEF(JS_ATOM_ReferenceError) + DEF(JS_ATOM_SyntaxError) + DEF(JS_ATOM_TypeError) + DEF(JS_ATOM_URIError) + DEF(JS_ATOM_InternalError) + DEF(JS_ATOM_AggregateError) +#undef DEF +}; + +static JSValue js_error_isError(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + return JS_NewBool(ctx, JS_IsError(ctx, argv[0])); +} + +static const JSCFunctionListEntry js_error_funcs[] = { + JS_CFUNC_DEF("isError", 1, js_error_isError), +}; + /* AggregateError */ /* used by C code. */ @@ -39999,8 +41220,6 @@ static JSValue js_array_concat(JSContext *ctx, JSValueConst this_val, #define special_filter 4 #define special_TA 8 -static int js_typed_array_get_length_internal(JSContext *ctx, JSValueConst obj); - static JSValue js_typed_array___speciesCreate(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); @@ -40018,7 +41237,7 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, val = JS_UNDEFINED; if (special & special_TA) { obj = JS_DupValue(ctx, this_val); - len = js_typed_array_get_length_internal(ctx, obj); + len = js_typed_array_get_length_unsafe(ctx, obj); if (len < 0) goto exception; } else { @@ -40141,8 +41360,10 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, goto exception; args[0] = ret; res = JS_Invoke(ctx, arr, JS_ATOM_set, 1, args); - if (check_exception_free(ctx, res)) + if (check_exception_free(ctx, res)) { + JS_FreeValue(ctx, arr); goto exception; + } JS_FreeValue(ctx, ret); ret = arr; } @@ -40173,7 +41394,7 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, val = JS_UNDEFINED; if (special & special_TA) { obj = JS_DupValue(ctx, this_val); - len = js_typed_array_get_length_internal(ctx, obj); + len = js_typed_array_get_length_unsafe(ctx, obj); if (len < 0) goto exception; } else { @@ -40645,6 +41866,31 @@ static JSValue js_array_push(JSContext *ctx, JSValueConst this_val, int i; int64_t len, from, newLen; + if (likely(JS_VALUE_GET_TAG(this_val) == JS_TAG_OBJECT && !unshift)) { + JSObject *p = JS_VALUE_GET_OBJ(this_val); + if (likely(p->class_id == JS_CLASS_ARRAY && p->fast_array && + p->extensible && + p->shape->proto == JS_VALUE_GET_OBJ(ctx->class_proto[JS_CLASS_ARRAY]) && + ctx->std_array_prototype && + JS_VALUE_GET_TAG(p->prop[0].u.value) == JS_TAG_INT && + JS_VALUE_GET_INT(p->prop[0].u.value) == p->u.array.count && + (get_shape_prop(p->shape)->flags & JS_PROP_WRITABLE) != 0)) { + /* fast case */ + uint32_t new_len; + new_len = p->u.array.count + argc; + if (likely(new_len <= INT32_MAX)) { + if (unlikely(new_len > p->u.array.u1.size)) { + if (expand_fast_array(ctx, p, new_len)) + return JS_EXCEPTION; + } + for(i = 0; i < argc; i++) + p->u.array.u.values[p->u.array.count + i] = JS_DupValue(ctx, argv[i]); + p->prop[0].u.value = JS_NewInt32(ctx, new_len); + p->u.array.count = new_len; + return JS_NewInt32(ctx, new_len); + } + } + } obj = JS_ToObject(ctx, this_val); if (js_get_length64(ctx, &len, obj)) goto exception; @@ -41383,23 +42629,6 @@ static void js_array_iterator_mark(JSRuntime *rt, JSValueConst val, } } -static JSValue js_create_array(JSContext *ctx, int len, JSValueConst *tab) -{ - JSValue obj; - int i; - - obj = JS_NewArray(ctx); - if (JS_IsException(obj)) - return JS_EXCEPTION; - for(i = 0; i < len; i++) { - if (JS_CreateDataPropertyUint32(ctx, obj, i, JS_DupValue(ctx, tab[i]), 0) < 0) { - JS_FreeValue(ctx, obj); - return JS_EXCEPTION; - } - } - return obj; -} - static JSValue js_create_array_iterator(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { @@ -41454,8 +42683,8 @@ static JSValue js_array_iterator_next(JSContext *ctx, JSValueConst this_val, p = JS_VALUE_GET_OBJ(it->obj); if (p->class_id >= JS_CLASS_UINT8C_ARRAY && p->class_id <= JS_CLASS_FLOAT64_ARRAY) { - if (typed_array_is_detached(ctx, p)) { - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + if (typed_array_is_oob(p)) { + JS_ThrowTypeErrorArrayBufferOOB(ctx); goto fail1; } len = p->u.array.count; @@ -41498,14 +42727,889 @@ static JSValue js_array_iterator_next(JSContext *ctx, JSValueConst this_val, } } +/* Iterator Wrap */ + +typedef struct JSIteratorWrapData { + JSValue wrapped_iter; + JSValue wrapped_next; +} JSIteratorWrapData; + +static void js_iterator_wrap_finalizer(JSRuntime *rt, JSValue val) +{ + JSObject *p = JS_VALUE_GET_OBJ(val); + JSIteratorWrapData *it = p->u.iterator_wrap_data; + if (it) { + JS_FreeValueRT(rt, it->wrapped_iter); + JS_FreeValueRT(rt, it->wrapped_next); + js_free_rt(rt, it); + } +} + +static void js_iterator_wrap_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func) +{ + JSObject *p = JS_VALUE_GET_OBJ(val); + JSIteratorWrapData *it = p->u.iterator_wrap_data; + if (it) { + JS_MarkValue(rt, it->wrapped_iter, mark_func); + JS_MarkValue(rt, it->wrapped_next, mark_func); + } +} + +static JSValue js_iterator_wrap_next(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, + int *pdone, int magic) +{ + JSIteratorWrapData *it; + JSValue method, ret; + it = JS_GetOpaque2(ctx, this_val, JS_CLASS_ITERATOR_WRAP); + if (!it) + return JS_EXCEPTION; + if (magic == GEN_MAGIC_NEXT) { + return JS_IteratorNext(ctx, it->wrapped_iter, it->wrapped_next, 0, NULL, pdone); + } else { + method = JS_GetProperty(ctx, it->wrapped_iter, JS_ATOM_return); + if (JS_IsException(method)) + return JS_EXCEPTION; + if (JS_IsNull(method) || JS_IsUndefined(method)) { + *pdone = TRUE; + return JS_UNDEFINED; + } + ret = JS_IteratorNext2(ctx, it->wrapped_iter, method, 0, NULL, pdone); + JS_FreeValue(ctx, method); + return ret; + } +} + +static const JSCFunctionListEntry js_iterator_wrap_proto_funcs[] = { + JS_ITERATOR_NEXT_DEF("next", 0, js_iterator_wrap_next, GEN_MAGIC_NEXT ), + JS_ITERATOR_NEXT_DEF("return", 0, js_iterator_wrap_next, GEN_MAGIC_RETURN ), +}; + +/* Iterator */ + +static JSValue js_iterator_constructor_getset(JSContext *ctx, + JSValueConst this_val, + int argc, JSValueConst *argv, + int magic, + JSValue *func_data) +{ + int ret; + + if (argc > 0) { // if setter + if (!JS_IsObject(argv[0])) + return JS_ThrowTypeErrorNotAnObject(ctx); + ret = JS_DefinePropertyValue(ctx, this_val, JS_ATOM_constructor, + JS_DupValue(ctx, argv[0]), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + if (ret < 0) + return JS_EXCEPTION; + return JS_UNDEFINED; + } else { + return JS_DupValue(ctx, func_data[0]); + } +} + +static JSValue js_iterator_constructor(JSContext *ctx, JSValueConst new_target, + int argc, JSValueConst *argv) +{ + JSObject *p; + + if (JS_TAG_OBJECT != JS_VALUE_GET_TAG(new_target)) + return JS_ThrowTypeError(ctx, "constructor requires 'new'"); + p = JS_VALUE_GET_OBJ(new_target); + if (p->class_id == JS_CLASS_C_FUNCTION && + p->u.cfunc.c_function.generic == js_iterator_constructor) { + return JS_ThrowTypeError(ctx, "abstract class not constructable"); + } + return js_create_from_ctor(ctx, new_target, JS_CLASS_ITERATOR); +} + +static JSValue js_iterator_from(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValueConst obj = argv[0]; + JSValue method, iter, wrapper; + JSIteratorWrapData *it; + int ret; + + if (!JS_IsObject(obj)) { + if (!JS_IsString(obj)) + return JS_ThrowTypeError(ctx, "Iterator.from called on non-object"); + } + method = JS_GetProperty(ctx, obj, JS_ATOM_Symbol_iterator); + if (JS_IsException(method)) + return JS_EXCEPTION; + if (JS_IsNull(method) || JS_IsUndefined(method)) { + iter = JS_DupValue(ctx, obj); + } else { + iter = JS_GetIterator2(ctx, obj, method); + JS_FreeValue(ctx, method); + if (JS_IsException(iter)) + return JS_EXCEPTION; + } + + wrapper = JS_UNDEFINED; + method = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(method)) + goto fail; + + ret = JS_OrdinaryIsInstanceOf(ctx, iter, ctx->iterator_ctor); + if (ret < 0) + goto fail; + if (ret) { + JS_FreeValue(ctx, method); + return iter; + } + + wrapper = JS_NewObjectClass(ctx, JS_CLASS_ITERATOR_WRAP); + if (JS_IsException(wrapper)) + goto fail; + it = js_malloc(ctx, sizeof(*it)); + if (!it) + goto fail; + it->wrapped_iter = iter; + it->wrapped_next = method; + JS_SetOpaque(wrapper, it); + return wrapper; + + fail: + JS_FreeValue(ctx, method); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, wrapper); + return JS_EXCEPTION; +} + +typedef enum JSIteratorHelperKindEnum { + JS_ITERATOR_HELPER_KIND_DROP, + JS_ITERATOR_HELPER_KIND_EVERY, + JS_ITERATOR_HELPER_KIND_FILTER, + JS_ITERATOR_HELPER_KIND_FIND, + JS_ITERATOR_HELPER_KIND_FLAT_MAP, + JS_ITERATOR_HELPER_KIND_FOR_EACH, + JS_ITERATOR_HELPER_KIND_MAP, + JS_ITERATOR_HELPER_KIND_SOME, + JS_ITERATOR_HELPER_KIND_TAKE, +} JSIteratorHelperKindEnum; + +typedef struct JSIteratorHelperData { + JSValue obj; + JSValue next; + JSValue func; // predicate (filter) or mapper (flatMap, map) + JSValue inner; // innerValue (flatMap) + int64_t count; // limit (drop, take) or counter (filter, map, flatMap) + JSIteratorHelperKindEnum kind : 8; + uint8_t executing : 1; + uint8_t done : 1; +} JSIteratorHelperData; + +static JSValue js_create_iterator_helper(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSValueConst func; + JSValue obj, method; + int64_t count; + JSIteratorHelperData *it; + + if (!JS_IsObject(this_val)) + return JS_ThrowTypeErrorNotAnObject(ctx); + func = JS_UNDEFINED; + count = 0; + + switch(magic) { + case JS_ITERATOR_HELPER_KIND_DROP: + case JS_ITERATOR_HELPER_KIND_TAKE: + { + JSValue v; + double dlimit; + v = JS_ToNumber(ctx, argv[0]); + if (JS_IsException(v)) + goto fail; + // Check for Infinity. + if (JS_ToFloat64(ctx, &dlimit, v)) { + JS_FreeValue(ctx, v); + goto fail; + } + if (isnan(dlimit)) { + JS_FreeValue(ctx, v); + goto range_error; + } + if (!isfinite(dlimit)) { + JS_FreeValue(ctx, v); + if (dlimit < 0) + goto range_error; + else + count = MAX_SAFE_INTEGER; + } else { + v = JS_ToIntegerFree(ctx, v); + if (JS_IsException(v)) + goto fail; + if (JS_ToInt64Free(ctx, &count, v)) + goto fail; + } + if (count < 0) + goto range_error; + } + break; + case JS_ITERATOR_HELPER_KIND_FILTER: + case JS_ITERATOR_HELPER_KIND_FLAT_MAP: + case JS_ITERATOR_HELPER_KIND_MAP: + { + func = argv[0]; + if (check_function(ctx, func)) + goto fail; + } + break; + default: + abort(); + break; + } + + method = JS_GetProperty(ctx, this_val, JS_ATOM_next); + if (JS_IsException(method)) + goto fail; + obj = JS_NewObjectClass(ctx, JS_CLASS_ITERATOR_HELPER); + if (JS_IsException(obj)) { + JS_FreeValue(ctx, method); + goto fail; + } + it = js_malloc(ctx, sizeof(*it)); + if (!it) { + JS_FreeValue(ctx, obj); + JS_FreeValue(ctx, method); + goto fail; + } + it->kind = magic; + it->obj = JS_DupValue(ctx, this_val); + it->func = JS_DupValue(ctx, func); + it->next = method; + it->inner = JS_UNDEFINED; + it->count = count; + it->executing = 0; + it->done = 0; + JS_SetOpaque(obj, it); + return obj; +range_error: + JS_ThrowRangeError(ctx, "must be positive"); +fail: + JS_IteratorClose(ctx, this_val, TRUE); + return JS_EXCEPTION; +} + +static JSValue js_iterator_proto_func(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSValue item, method, ret, func, index_val, r; + JSValueConst args[2]; + int64_t idx; + int done; + + if (!JS_IsObject(this_val)) + return JS_ThrowTypeErrorNotAnObject(ctx); + func = JS_UNDEFINED; + method = JS_UNDEFINED; + + if (check_function(ctx, argv[0])) + goto fail; + func = JS_DupValue(ctx, argv[0]); + method = JS_GetProperty(ctx, this_val, JS_ATOM_next); + if (JS_IsException(method)) + goto fail_no_close; + + r = JS_UNDEFINED; + + switch(magic) { + case JS_ITERATOR_HELPER_KIND_EVERY: + { + r = JS_TRUE; + for (idx = 0; /*empty*/; idx++) { + item = JS_IteratorNext(ctx, this_val, method, 0, NULL, &done); + if (JS_IsException(item)) + goto fail_no_close; + if (done) + break; + index_val = JS_NewInt64(ctx, idx); + args[0] = item; + args[1] = index_val; + ret = JS_Call(ctx, func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, index_val); + if (JS_IsException(ret)) + goto fail; + if (!JS_ToBoolFree(ctx, ret)) { + if (JS_IteratorClose(ctx, this_val, FALSE) < 0) + r = JS_EXCEPTION; + else + r = JS_FALSE; + break; + } + index_val = JS_UNDEFINED; + ret = JS_UNDEFINED; + item = JS_UNDEFINED; + } + } + break; + case JS_ITERATOR_HELPER_KIND_FIND: + { + for (idx = 0; /*empty*/; idx++) { + item = JS_IteratorNext(ctx, this_val, method, 0, NULL, &done); + if (JS_IsException(item)) + goto fail_no_close; + if (done) + break; + index_val = JS_NewInt64(ctx, idx); + args[0] = item; + args[1] = index_val; + ret = JS_Call(ctx, func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, index_val); + if (JS_IsException(ret)) { + JS_FreeValue(ctx, item); + goto fail; + } + if (JS_ToBoolFree(ctx, ret)) { + if (JS_IteratorClose(ctx, this_val, FALSE) < 0) { + JS_FreeValue(ctx, item); + r = JS_EXCEPTION; + } else { + r = item; + } + break; + } + JS_FreeValue(ctx, item); + index_val = JS_UNDEFINED; + ret = JS_UNDEFINED; + item = JS_UNDEFINED; + } + } + break; + case JS_ITERATOR_HELPER_KIND_FOR_EACH: + { + for (idx = 0; /*empty*/; idx++) { + item = JS_IteratorNext(ctx, this_val, method, 0, NULL, &done); + if (JS_IsException(item)) + goto fail_no_close; + if (done) + break; + index_val = JS_NewInt64(ctx, idx); + args[0] = item; + args[1] = index_val; + ret = JS_Call(ctx, func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, index_val); + if (JS_IsException(ret)) + goto fail; + JS_FreeValue(ctx, ret); + index_val = JS_UNDEFINED; + ret = JS_UNDEFINED; + item = JS_UNDEFINED; + } + } + break; + case JS_ITERATOR_HELPER_KIND_SOME: + { + r = JS_FALSE; + for (idx = 0; /*empty*/; idx++) { + item = JS_IteratorNext(ctx, this_val, method, 0, NULL, &done); + if (JS_IsException(item)) + goto fail_no_close; + if (done) + break; + index_val = JS_NewInt64(ctx, idx); + args[0] = item; + args[1] = index_val; + ret = JS_Call(ctx, func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, index_val); + if (JS_IsException(ret)) + goto fail; + if (JS_ToBoolFree(ctx, ret)) { + if (JS_IteratorClose(ctx, this_val, FALSE) < 0) + r = JS_EXCEPTION; + else + r = JS_TRUE; + break; + } + index_val = JS_UNDEFINED; + ret = JS_UNDEFINED; + item = JS_UNDEFINED; + } + } + break; + default: + abort(); + break; + } + + JS_FreeValue(ctx, func); + JS_FreeValue(ctx, method); + return r; + fail: + JS_IteratorClose(ctx, this_val, TRUE); + fail_no_close: + JS_FreeValue(ctx, func); + JS_FreeValue(ctx, method); + return JS_EXCEPTION; +} + +static JSValue js_iterator_proto_reduce(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue item, method, ret, func, index_val, acc; + JSValueConst args[3]; + int64_t idx; + int done; + + if (!JS_IsObject(this_val)) + return JS_ThrowTypeErrorNotAnObject(ctx); + acc = JS_UNDEFINED; + func = JS_UNDEFINED; + method = JS_UNDEFINED; + if (check_function(ctx, argv[0])) + goto exception; + func = JS_DupValue(ctx, argv[0]); + method = JS_GetProperty(ctx, this_val, JS_ATOM_next); + if (JS_IsException(method)) + goto exception; + if (argc > 1) { + acc = JS_DupValue(ctx, argv[1]); + idx = 0; + } else { + acc = JS_IteratorNext(ctx, this_val, method, 0, NULL, &done); + if (JS_IsException(acc)) + goto exception_no_close; + if (done) { + JS_ThrowTypeError(ctx, "empty iterator"); + goto exception; + } + idx = 1; + } + for (/* empty */; /*empty*/; idx++) { + item = JS_IteratorNext(ctx, this_val, method, 0, NULL, &done); + if (JS_IsException(item)) + goto exception_no_close; + if (done) + break; + index_val = JS_NewInt64(ctx, idx); + args[0] = acc; + args[1] = item; + args[2] = index_val; + ret = JS_Call(ctx, func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, index_val); + if (JS_IsException(ret)) + goto exception; + JS_FreeValue(ctx, acc); + acc = ret; + index_val = JS_UNDEFINED; + ret = JS_UNDEFINED; + item = JS_UNDEFINED; + } + JS_FreeValue(ctx, func); + JS_FreeValue(ctx, method); + return acc; + exception: + JS_IteratorClose(ctx, this_val, TRUE); + exception_no_close: + JS_FreeValue(ctx, acc); + JS_FreeValue(ctx, func); + JS_FreeValue(ctx, method); + return JS_EXCEPTION; +} + +static JSValue js_iterator_proto_toArray(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue item, method, result; + int64_t idx; + int done; + + result = JS_UNDEFINED; + if (!JS_IsObject(this_val)) + return JS_ThrowTypeErrorNotAnObject(ctx); + method = JS_GetProperty(ctx, this_val, JS_ATOM_next); + if (JS_IsException(method)) + return JS_EXCEPTION; + result = JS_NewArray(ctx); + if (JS_IsException(result)) + goto exception; + for (idx = 0; /*empty*/; idx++) { + item = JS_IteratorNext(ctx, this_val, method, 0, NULL, &done); + if (JS_IsException(item)) + goto exception; + if (done) + break; + if (JS_DefinePropertyValueInt64(ctx, result, idx, item, + JS_PROP_C_W_E | JS_PROP_THROW) < 0) + goto exception; + } + if (JS_SetProperty(ctx, result, JS_ATOM_length, JS_NewUint32(ctx, idx)) < 0) + goto exception; + JS_FreeValue(ctx, method); + return result; +exception: + JS_FreeValue(ctx, result); + JS_FreeValue(ctx, method); + return JS_EXCEPTION; +} + static JSValue js_iterator_proto_iterator(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { return JS_DupValue(ctx, this_val); } +static JSValue js_iterator_proto_get_toStringTag(JSContext *ctx, JSValueConst this_val) +{ + return JS_AtomToString(ctx, JS_ATOM_Iterator); +} + +static JSValue js_iterator_proto_set_toStringTag(JSContext *ctx, JSValueConst this_val, JSValueConst val) +{ + int res; + + if (!JS_IsObject(this_val)) + return JS_ThrowTypeErrorNotAnObject(ctx); + if (js_same_value(ctx, this_val, ctx->class_proto[JS_CLASS_ITERATOR])) + return JS_ThrowTypeError(ctx, "Cannot assign to read only property"); + res = JS_GetOwnProperty(ctx, NULL, this_val, JS_ATOM_Symbol_toStringTag); + if (res < 0) + return JS_EXCEPTION; + if (res) { + if (JS_SetProperty(ctx, this_val, JS_ATOM_Symbol_toStringTag, JS_DupValue(ctx, val)) < 0) + return JS_EXCEPTION; + } else { + if (JS_DefinePropertyValue(ctx, this_val, JS_ATOM_Symbol_toStringTag, JS_DupValue(ctx, val), JS_PROP_C_W_E) < 0) + return JS_EXCEPTION; + } + return JS_UNDEFINED; +} + +/* Iterator Helper */ + +static void js_iterator_helper_finalizer(JSRuntime *rt, JSValue val) +{ + JSObject *p = JS_VALUE_GET_OBJ(val); + JSIteratorHelperData *it = p->u.iterator_helper_data; + if (it) { + JS_FreeValueRT(rt, it->obj); + JS_FreeValueRT(rt, it->func); + JS_FreeValueRT(rt, it->next); + JS_FreeValueRT(rt, it->inner); + js_free_rt(rt, it); + } +} + +static void js_iterator_helper_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func) +{ + JSObject *p = JS_VALUE_GET_OBJ(val); + JSIteratorHelperData *it = p->u.iterator_helper_data; + if (it) { + JS_MarkValue(rt, it->obj, mark_func); + JS_MarkValue(rt, it->func, mark_func); + JS_MarkValue(rt, it->next, mark_func); + JS_MarkValue(rt, it->inner, mark_func); + } +} + +static JSValue js_iterator_helper_next(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, + int *pdone, int magic) +{ + JSIteratorHelperData *it; + JSValue ret; + + *pdone = FALSE; + + it = JS_GetOpaque2(ctx, this_val, JS_CLASS_ITERATOR_HELPER); + if (!it) + return JS_EXCEPTION; + if (it->executing) + return JS_ThrowTypeError(ctx, "cannot invoke a running iterator"); + if (it->done) { + *pdone = TRUE; + return JS_UNDEFINED; + } + + it->executing = 1; + + switch (it->kind) { + case JS_ITERATOR_HELPER_KIND_DROP: + { + JSValue item, method; + if (magic == GEN_MAGIC_NEXT) { + method = JS_DupValue(ctx, it->next); + } else { + method = JS_GetProperty(ctx, it->obj, JS_ATOM_return); + if (JS_IsException(method)) + goto fail; + } + while (it->count > 0) { + it->count--; + item = JS_IteratorNext(ctx, it->obj, method, 0, NULL, pdone); + if (JS_IsException(item)) { + JS_FreeValue(ctx, method); + goto fail_no_close; + } + JS_FreeValue(ctx, item); + if (magic == GEN_MAGIC_RETURN) + *pdone = TRUE; + if (*pdone) { + JS_FreeValue(ctx, method); + ret = JS_UNDEFINED; + goto done; + } + } + + item = JS_IteratorNext(ctx, it->obj, method, 0, NULL, pdone); + JS_FreeValue(ctx, method); + if (JS_IsException(item)) + goto fail_no_close; + ret = item; + goto done; + } + break; + case JS_ITERATOR_HELPER_KIND_FILTER: + { + JSValue item, method, selected, index_val; + JSValueConst args[2]; + if (magic == GEN_MAGIC_NEXT) { + method = JS_DupValue(ctx, it->next); + } else { + method = JS_GetProperty(ctx, it->obj, JS_ATOM_return); + if (JS_IsException(method)) + goto fail; + } + filter_again: + item = JS_IteratorNext(ctx, it->obj, method, 0, NULL, pdone); + if (JS_IsException(item)) { + JS_FreeValue(ctx, method); + goto fail_no_close; + } + if (*pdone || magic == GEN_MAGIC_RETURN) { + JS_FreeValue(ctx, method); + ret = item; + goto done; + } + index_val = JS_NewInt64(ctx, it->count++); + args[0] = item; + args[1] = index_val; + selected = JS_Call(ctx, it->func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, index_val); + if (JS_IsException(selected)) { + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, method); + goto fail; + } + if (JS_ToBoolFree(ctx, selected)) { + JS_FreeValue(ctx, method); + ret = item; + goto done; + } + JS_FreeValue(ctx, item); + goto filter_again; + } + break; + case JS_ITERATOR_HELPER_KIND_FLAT_MAP: + { + JSValue item, method, index_val, iter; + JSValueConst args[2]; + flat_map_again: + if (JS_IsUndefined(it->inner)) { + if (magic == GEN_MAGIC_NEXT) { + method = JS_DupValue(ctx, it->next); + } else { + method = JS_GetProperty(ctx, it->obj, JS_ATOM_return); + if (JS_IsException(method)) + goto fail; + } + item = JS_IteratorNext(ctx, it->obj, method, 0, NULL, pdone); + JS_FreeValue(ctx, method); + if (JS_IsException(item)) + goto fail_no_close; + if (*pdone || magic == GEN_MAGIC_RETURN) { + ret = item; + goto done; + } + index_val = JS_NewInt64(ctx, it->count++); + args[0] = item; + args[1] = index_val; + ret = JS_Call(ctx, it->func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, index_val); + if (JS_IsException(ret)) + goto fail; + if (!JS_IsObject(ret)) { + JS_FreeValue(ctx, ret); + JS_ThrowTypeError(ctx, "not an object"); + goto fail; + } + method = JS_GetProperty(ctx, ret, JS_ATOM_Symbol_iterator); + if (JS_IsException(method)) { + JS_FreeValue(ctx, ret); + goto fail; + } + if (JS_IsNull(method) || JS_IsUndefined(method)) { + JS_FreeValue(ctx, method); + iter = ret; + } else { + iter = JS_GetIterator2(ctx, ret, method); + JS_FreeValue(ctx, method); + JS_FreeValue(ctx, ret); + if (JS_IsException(iter)) + goto fail; + } + + it->inner = iter; + } + + if (magic == GEN_MAGIC_NEXT) + method = JS_GetProperty(ctx, it->inner, JS_ATOM_next); + else + method = JS_GetProperty(ctx, it->inner, JS_ATOM_return); + if (JS_IsException(method)) { + inner_fail: + JS_IteratorClose(ctx, it->inner, FALSE); + JS_FreeValue(ctx, it->inner); + it->inner = JS_UNDEFINED; + goto fail; + } + if (magic == GEN_MAGIC_RETURN && (JS_IsUndefined(method) || JS_IsNull(method))) { + goto inner_end; + } else { + item = JS_IteratorNext(ctx, it->inner, method, 0, NULL, pdone); + JS_FreeValue(ctx, method); + if (JS_IsException(item)) + goto inner_fail; + } + if (*pdone) { + inner_end: + *pdone = FALSE; // The outer iterator must continue. + JS_IteratorClose(ctx, it->inner, FALSE); + JS_FreeValue(ctx, it->inner); + it->inner = JS_UNDEFINED; + goto flat_map_again; + } + ret = item; + goto done; + } + break; + case JS_ITERATOR_HELPER_KIND_MAP: + { + JSValue item, method, index_val; + JSValueConst args[2]; + if (magic == GEN_MAGIC_NEXT) { + method = JS_DupValue(ctx, it->next); + } else { + method = JS_GetProperty(ctx, it->obj, JS_ATOM_return); + if (JS_IsException(method)) + goto fail; + } + item = JS_IteratorNext(ctx, it->obj, method, 0, NULL, pdone); + JS_FreeValue(ctx, method); + if (JS_IsException(item)) + goto fail_no_close; + if (*pdone || magic == GEN_MAGIC_RETURN) { + ret = item; + goto done; + } + index_val = JS_NewInt64(ctx, it->count++); + args[0] = item; + args[1] = index_val; + ret = JS_Call(ctx, it->func, JS_UNDEFINED, countof(args), args); + JS_FreeValue(ctx, index_val); + if (JS_IsException(ret)) + goto fail; + goto done; + } + break; + case JS_ITERATOR_HELPER_KIND_TAKE: + { + JSValue item, method; + if (it->count > 0) { + if (magic == GEN_MAGIC_NEXT) { + method = JS_DupValue(ctx, it->next); + } else { + method = JS_GetProperty(ctx, it->obj, JS_ATOM_return); + if (JS_IsException(method)) + goto fail; + } + it->count--; + item = JS_IteratorNext(ctx, it->obj, method, 0, NULL, pdone); + JS_FreeValue(ctx, method); + if (JS_IsException(item)) + goto fail_no_close; + ret = item; + goto done; + } + + *pdone = TRUE; + if (JS_IteratorClose(ctx, it->obj, FALSE)) + ret = JS_EXCEPTION; + else + ret = JS_UNDEFINED; + goto done; + } + break; + default: + abort(); + } + + done: + it->done = magic == GEN_MAGIC_NEXT ? *pdone : 1; + it->executing = 0; + return ret; + fail: + /* close the iterator object, preserving pending exception */ + JS_IteratorClose(ctx, it->obj, TRUE); + fail_no_close: + ret = JS_EXCEPTION; + goto done; +} + +static const JSCFunctionListEntry js_iterator_funcs[] = { + JS_CFUNC_DEF("from", 1, js_iterator_from ), +}; + static const JSCFunctionListEntry js_iterator_proto_funcs[] = { + JS_CFUNC_MAGIC_DEF("drop", 1, js_create_iterator_helper, JS_ITERATOR_HELPER_KIND_DROP ), + JS_CFUNC_MAGIC_DEF("filter", 1, js_create_iterator_helper, JS_ITERATOR_HELPER_KIND_FILTER ), + JS_CFUNC_MAGIC_DEF("flatMap", 1, js_create_iterator_helper, JS_ITERATOR_HELPER_KIND_FLAT_MAP ), + JS_CFUNC_MAGIC_DEF("map", 1, js_create_iterator_helper, JS_ITERATOR_HELPER_KIND_MAP ), + JS_CFUNC_MAGIC_DEF("take", 1, js_create_iterator_helper, JS_ITERATOR_HELPER_KIND_TAKE ), + JS_CFUNC_MAGIC_DEF("every", 1, js_iterator_proto_func, JS_ITERATOR_HELPER_KIND_EVERY ), + JS_CFUNC_MAGIC_DEF("find", 1, js_iterator_proto_func, JS_ITERATOR_HELPER_KIND_FIND), + JS_CFUNC_MAGIC_DEF("forEach", 1, js_iterator_proto_func, JS_ITERATOR_HELPER_KIND_FOR_EACH ), + JS_CFUNC_MAGIC_DEF("some", 1, js_iterator_proto_func, JS_ITERATOR_HELPER_KIND_SOME ), + JS_CFUNC_DEF("reduce", 1, js_iterator_proto_reduce ), + JS_CFUNC_DEF("toArray", 0, js_iterator_proto_toArray ), JS_CFUNC_DEF("[Symbol.iterator]", 0, js_iterator_proto_iterator ), + JS_CGETSET_DEF("[Symbol.toStringTag]", js_iterator_proto_get_toStringTag, js_iterator_proto_set_toStringTag), +}; + +static const JSCFunctionListEntry js_iterator_helper_proto_funcs[] = { + JS_ITERATOR_NEXT_DEF("next", 0, js_iterator_helper_next, GEN_MAGIC_NEXT ), + JS_ITERATOR_NEXT_DEF("return", 0, js_iterator_helper_next, GEN_MAGIC_RETURN ), + JS_PROP_STRING_DEF("[Symbol.toStringTag]", "Iterator Helper", JS_PROP_CONFIGURABLE ), +}; + +static const JSCFunctionListEntry js_array_unscopables_funcs[] = { + JS_PROP_BOOL_DEF("at", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("copyWithin", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("entries", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("fill", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("find", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("findIndex", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("findLast", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("findLastIndex", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("flat", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("flatMap", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("includes", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("keys", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("toReversed", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("toSorted", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("toSpliced", TRUE, JS_PROP_C_W_E), + JS_PROP_BOOL_DEF("values", TRUE, JS_PROP_C_W_E), }; static const JSCFunctionListEntry js_array_proto_funcs[] = { @@ -41548,6 +43652,7 @@ static const JSCFunctionListEntry js_array_proto_funcs[] = { JS_ALIAS_DEF("[Symbol.iterator]", "values" ), JS_CFUNC_MAGIC_DEF("keys", 0, js_create_array_iterator, JS_ITERATOR_KIND_KEY ), JS_CFUNC_MAGIC_DEF("entries", 0, js_create_array_iterator, JS_ITERATOR_KIND_KEY_AND_VALUE ), + JS_OBJECT_DEF("[Symbol.unscopables]", js_array_unscopables_funcs, countof(js_array_unscopables_funcs), JS_PROP_CONFIGURABLE ), }; static const JSCFunctionListEntry js_array_iterator_proto_funcs[] = { @@ -43171,12 +45276,6 @@ static JSValue js_string_trim(JSContext *ctx, JSValueConst this_val, return ret; } -static JSValue js_string___quote(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) -{ - return JS_ToQuotedString(ctx, this_val); -} - /* return 0 if before the first char */ static int string_prevc(JSString *p, int *pidx) { @@ -43664,7 +45763,6 @@ static const JSCFunctionListEntry js_string_proto_funcs[] = { JS_ALIAS_DEF("trimLeft", "trimStart" ), JS_CFUNC_DEF("toString", 0, js_string_toString ), JS_CFUNC_DEF("valueOf", 0, js_string_toString ), - JS_CFUNC_DEF("__quote", 1, js_string___quote ), JS_CFUNC_MAGIC_DEF("toLowerCase", 0, js_string_toLowerCase, 1 ), JS_CFUNC_MAGIC_DEF("toUpperCase", 0, js_string_toLowerCase, 0 ), JS_CFUNC_MAGIC_DEF("toLocaleLowerCase", 0, js_string_toLowerCase, 1 ), @@ -43698,10 +45796,10 @@ static const JSCFunctionListEntry js_string_proto_normalize[] = { JS_CFUNC_DEF("localeCompare", 1, js_string_localeCompare ), }; -void JS_AddIntrinsicStringNormalize(JSContext *ctx) +int JS_AddIntrinsicStringNormalize(JSContext *ctx) { - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_STRING], js_string_proto_normalize, - countof(js_string_proto_normalize)); + return JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_STRING], js_string_proto_normalize, + countof(js_string_proto_normalize)); } /* Math */ @@ -43849,6 +45947,11 @@ static JSValue js_math_hypot(JSContext *ctx, JSValueConst this_val, return JS_NewFloat64(ctx, r); } +static double js_math_f16round(double a) +{ + return fromfp16(tofp16(a)); +} + static double js_math_fround(double a) { return (float)a; @@ -43883,6 +45986,261 @@ static JSValue js_math_clz32(JSContext *ctx, JSValueConst this_val, return JS_NewInt32(ctx, r); } +typedef enum { + SUM_PRECISE_STATE_FINITE, + SUM_PRECISE_STATE_INFINITY, + SUM_PRECISE_STATE_MINUS_INFINITY, /* must be after SUM_PRECISE_STATE_INFINITY */ + SUM_PRECISE_STATE_NAN, /* must be after SUM_PRECISE_STATE_MINUS_INFINITY */ +} SumPreciseStateEnum; + +#define SP_LIMB_BITS 56 +#define SP_RND_BITS (SP_LIMB_BITS - 53) +/* we add one extra limb to avoid having to test for overflows during the sum */ +#define SUM_PRECISE_ACC_LEN 39 + +#define SUM_PRECISE_COUNTER_INIT 250 + +typedef struct { + SumPreciseStateEnum state; + uint32_t counter; + int n_limbs; /* 'acc' contains n_limbs and is not necessarily + acc[n_limb - 1] may be 0. 0 indicates minus zero + result when state = SUM_PRECISE_STATE_FINITE */ + int64_t acc[SUM_PRECISE_ACC_LEN]; +} SumPreciseState; + +static void sum_precise_init(SumPreciseState *s) +{ + memset(s->acc, 0, sizeof(s->acc)); + s->state = SUM_PRECISE_STATE_FINITE; + s->counter = SUM_PRECISE_COUNTER_INIT; + s->n_limbs = 0; +} + +static void sum_precise_renorm(SumPreciseState *s) +{ + int64_t v, carry; + int i; + + carry = 0; + for(i = 0; i < s->n_limbs; i++) { + v = s->acc[i] + carry; + s->acc[i] = v & (((uint64_t)1 << SP_LIMB_BITS) - 1); + carry = v >> SP_LIMB_BITS; + } + /* we add a failsafe but it should be never reached in a + reasonnable amount of time */ + if (carry != 0 && s->n_limbs < SUM_PRECISE_ACC_LEN) + s->acc[s->n_limbs++] = carry; +} + +static void sum_precise_add(SumPreciseState *s, double d) +{ + uint64_t a, m, a0, a1; + int sgn, e, p; + unsigned int shift; + + a = float64_as_uint64(d); + sgn = a >> 63; + e = (a >> 52) & ((1 << 11) - 1); + m = a & (((uint64_t)1 << 52) - 1); + if (unlikely(e == 2047)) { + if (m == 0) { + /* +/- infinity */ + if (s->state == SUM_PRECISE_STATE_NAN || + (s->state == SUM_PRECISE_STATE_MINUS_INFINITY && !sgn) || + (s->state == SUM_PRECISE_STATE_INFINITY && sgn)) { + s->state = SUM_PRECISE_STATE_NAN; + } else { + s->state = SUM_PRECISE_STATE_INFINITY + sgn; + } + } else { + /* NaN */ + s->state = SUM_PRECISE_STATE_NAN; + } + } else if (e == 0) { + if (likely(m == 0)) { + /* zero */ + if (s->n_limbs == 0 && !sgn) + s->n_limbs = 1; + } else { + /* subnormal */ + p = 0; + shift = 0; + goto add; + } + } else { + /* Note: we sum even if state != SUM_PRECISE_STATE_FINITE to + avoid tests */ + m |= (uint64_t)1 << 52; + shift = e - 1; + /* 'p' is the position of a0 in acc. The division is normally + implementation as a multiplication by the compiler. */ + p = shift / SP_LIMB_BITS; + shift %= SP_LIMB_BITS; + add: + a0 = (m << shift) & (((uint64_t)1 << SP_LIMB_BITS) - 1); + a1 = m >> (SP_LIMB_BITS - shift); + if (!sgn) { + s->acc[p] += a0; + s->acc[p + 1] += a1; + } else { + s->acc[p] -= a0; + s->acc[p + 1] -= a1; + } + s->n_limbs = max_int(s->n_limbs, p + 2); + + if (unlikely(--s->counter == 0)) { + s->counter = SUM_PRECISE_COUNTER_INIT; + sum_precise_renorm(s); + } + } +} + +static double sum_precise_get_result(SumPreciseState *s) +{ + int n, shift, e, p, is_neg; + uint64_t m, addend; + + if (s->state != SUM_PRECISE_STATE_FINITE) { + switch(s->state) { + default: + case SUM_PRECISE_STATE_INFINITY: + return INFINITY; + case SUM_PRECISE_STATE_MINUS_INFINITY: + return -INFINITY; + case SUM_PRECISE_STATE_NAN: + return NAN; + } + } + + sum_precise_renorm(s); + + /* extract the sign and absolute value */ +#if 0 + { + int i; + printf("len=%d:", s->n_limbs); + for(i = s->n_limbs - 1; i >= 0; i--) + printf(" %014lx", s->acc[i]); + printf("\n"); + } +#endif + n = s->n_limbs; + /* minus zero result */ + if (n == 0) + return -0.0; + + /* normalize */ + while (n > 0 && s->acc[n - 1] == 0) + n--; + /* zero result. The spec tells it is always positive in the finite case */ + if (n == 0) + return 0.0; + is_neg = (s->acc[n - 1] < 0); + if (is_neg) { + uint64_t v, carry; + int i; + /* negate */ + /* XXX: do it only when needed */ + carry = 1; + for(i = 0; i < n - 1; i++) { + v = (((uint64_t)1 << SP_LIMB_BITS) - 1) - s->acc[i] + carry; + carry = v >> SP_LIMB_BITS; + s->acc[i] = v & (((uint64_t)1 << SP_LIMB_BITS) - 1); + } + s->acc[n - 1] = -s->acc[n - 1] + carry - 1; + while (n > 1 && s->acc[n - 1] == 0) + n--; + } + /* subnormal case */ + if (n == 1 && s->acc[0] < ((uint64_t)1 << 52)) + return uint64_as_float64(((uint64_t)is_neg << 63) | s->acc[0]); + /* normal case */ + e = n * SP_LIMB_BITS; + p = n - 1; + m = s->acc[p]; + shift = clz64(m) - (64 - SP_LIMB_BITS); + e = e - shift - 52; + if (shift != 0) { + m <<= shift; + if (p > 0) { + int shift1; + uint64_t nz; + p--; + shift1 = SP_LIMB_BITS - shift; + nz = s->acc[p] & (((uint64_t)1 << shift1) - 1); + m = m | (s->acc[p] >> shift1) | (nz != 0); + } + } + if ((m & ((1 << SP_RND_BITS) - 1)) == (1 << (SP_RND_BITS - 1))) { + /* see if the LSB part is non zero for the final rounding */ + while (p > 0) { + p--; + if (s->acc[p] != 0) { + m |= 1; + break; + } + } + } + /* rounding to nearest with ties to even */ + addend = (1 << (SP_RND_BITS - 1)) - 1 + ((m >> SP_RND_BITS) & 1); + m = (m + addend) >> SP_RND_BITS; + /* handle overflow in the rounding */ + if (m == ((uint64_t)1 << 53)) + e++; + if (unlikely(e >= 2047)) { + /* infinity */ + return uint64_as_float64(((uint64_t)is_neg << 63) | ((uint64_t)2047 << 52)); + } else { + m &= (((uint64_t)1 << 52) - 1); + return uint64_as_float64(((uint64_t)is_neg << 63) | ((uint64_t)e << 52) | m); + } +} + +static JSValue js_math_sumPrecise(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue iter, next, item, ret; + uint32_t tag; + int done; + double d; + SumPreciseState s_s, *s = &s_s; + + iter = JS_GetIterator(ctx, argv[0], FALSE); + if (JS_IsException(iter)) + return JS_EXCEPTION; + ret = JS_EXCEPTION; + next = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(next)) + goto fail; + sum_precise_init(s); + for (;;) { + item = JS_IteratorNext(ctx, iter, next, 0, NULL, &done); + if (JS_IsException(item)) + goto fail; + if (done) + break; + tag = JS_VALUE_GET_TAG(item); + if (JS_TAG_IS_FLOAT64(tag)) { + d = JS_VALUE_GET_FLOAT64(item); + } else if (tag == JS_TAG_INT) { + d = JS_VALUE_GET_INT(item); + } else { + JS_FreeValue(ctx, item); + JS_ThrowTypeError(ctx, "not a number"); + JS_IteratorClose(ctx, iter, TRUE); + goto fail; + } + sum_precise_add(s, d); + } + ret = __JS_NewFloat64(ctx, sum_precise_get_result(s)); +fail: + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, next); + return ret; +} + /* xorshift* random number generator by Marsaglia */ static uint64_t xorshift64star(uint64_t *pstate) { @@ -43952,9 +46310,11 @@ static const JSCFunctionListEntry js_math_funcs[] = { JS_CFUNC_SPECIAL_DEF("cbrt", 1, f_f, cbrt ), JS_CFUNC_DEF("hypot", 2, js_math_hypot ), JS_CFUNC_DEF("random", 0, js_math_random ), + JS_CFUNC_SPECIAL_DEF("f16round", 1, f_f, js_math_f16round ), JS_CFUNC_SPECIAL_DEF("fround", 1, f_f, js_math_fround ), JS_CFUNC_DEF("imul", 2, js_math_imul ), JS_CFUNC_DEF("clz32", 1, js_math_clz32 ), + JS_CFUNC_DEF("sumPrecise", 1, js_math_sumPrecise ), JS_PROP_STRING_DEF("[Symbol.toStringTag]", "Math", JS_PROP_CONFIGURABLE ), JS_PROP_DOUBLE_DEF("E", 2.718281828459045, 0 ), JS_PROP_DOUBLE_DEF("LN10", 2.302585092994046, 0 ), @@ -44007,9 +46367,13 @@ static int getTimezoneOffset(int64_t time) time_t gm_ti, loc_ti; tm = gmtime(&ti); + if (!tm) + return 0; gm_ti = mktime(tm); tm = localtime(&ti); + if (!tm) + return 0; loc_ti = mktime(tm); res = (gm_ti - loc_ti) / 60; @@ -44074,8 +46438,10 @@ static void js_regexp_finalizer(JSRuntime *rt, JSValue val) { JSObject *p = JS_VALUE_GET_OBJ(val); JSRegExp *re = &p->u.regexp; - JS_FreeValueRT(rt, JS_MKPTR(JS_TAG_STRING, re->bytecode)); - JS_FreeValueRT(rt, JS_MKPTR(JS_TAG_STRING, re->pattern)); + if (re->bytecode != NULL) + JS_FreeValueRT(rt, JS_MKPTR(JS_TAG_STRING, re->bytecode)); + if (re->pattern != NULL) + JS_FreeValueRT(rt, JS_MKPTR(JS_TAG_STRING, re->pattern)); } /* create a string containing the RegExp bytecode */ @@ -44116,6 +46482,9 @@ static JSValue js_compile_regexp(JSContext *ctx, JSValueConst pattern, case 'u': mask = LRE_FLAG_UNICODE; break; + case 'v': + mask = LRE_FLAG_UNICODE_SETS; + break; case 'y': mask = LRE_FLAG_STICKY; break; @@ -44125,14 +46494,20 @@ static JSValue js_compile_regexp(JSContext *ctx, JSValueConst pattern, if ((re_flags & mask) != 0) { bad_flags: JS_FreeCString(ctx, str); - return JS_ThrowSyntaxError(ctx, "invalid regular expression flags"); + goto bad_flags1; } re_flags |= mask; } JS_FreeCString(ctx, str); } - str = JS_ToCStringLen2(ctx, &len, pattern, !(re_flags & LRE_FLAG_UNICODE)); + /* 'u' and 'v' cannot be both set */ + if ((re_flags & LRE_FLAG_UNICODE_SETS) && (re_flags & LRE_FLAG_UNICODE)) { + bad_flags1: + return JS_ThrowSyntaxError(ctx, "invalid regular expression flags"); + } + + str = JS_ToCStringLen2(ctx, &len, pattern, !(re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS))); if (!str) return JS_EXCEPTION; re_bytecode_buf = lre_compile(&re_bytecode_len, error_msg, @@ -44148,32 +46523,29 @@ static JSValue js_compile_regexp(JSContext *ctx, JSValueConst pattern, return ret; } -/* create a RegExp object from a string containing the RegExp bytecode - and the source pattern */ -static JSValue js_regexp_constructor_internal(JSContext *ctx, JSValueConst ctor, - JSValue pattern, JSValue bc) +/* set the RegExp fields */ +static JSValue js_regexp_set_internal(JSContext *ctx, + JSValue obj, + JSValue pattern, JSValue bc) { - JSValue obj; JSObject *p; JSRegExp *re; /* sanity check */ - if (JS_VALUE_GET_TAG(bc) != JS_TAG_STRING || - JS_VALUE_GET_TAG(pattern) != JS_TAG_STRING) { + if (unlikely(JS_VALUE_GET_TAG(bc) != JS_TAG_STRING || + JS_VALUE_GET_TAG(pattern) != JS_TAG_STRING)) { JS_ThrowTypeError(ctx, "string expected"); - fail: + JS_FreeValue(ctx, obj); JS_FreeValue(ctx, bc); JS_FreeValue(ctx, pattern); return JS_EXCEPTION; } - obj = js_create_from_ctor(ctx, ctor, JS_CLASS_REGEXP); - if (JS_IsException(obj)) - goto fail; p = JS_VALUE_GET_OBJ(obj); re = &p->u.regexp; re->pattern = JS_VALUE_GET_STRING(pattern); re->bytecode = JS_VALUE_GET_STRING(bc); + /* Note: cannot fail because the field is preallocated */ JS_DefinePropertyValue(ctx, obj, JS_ATOM_lastIndex, JS_NewInt32(ctx, 0), JS_PROP_WRITABLE); return obj; @@ -44210,7 +46582,7 @@ static int js_is_regexp(JSContext *ctx, JSValueConst obj) static JSValue js_regexp_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) { - JSValue pattern, flags, bc, val; + JSValue pattern, flags, bc, val, obj = JS_UNDEFINED; JSValueConst pat, flags1; JSRegExp *re; int pat_is_regexp; @@ -44236,18 +46608,19 @@ static JSValue js_regexp_constructor(JSContext *ctx, JSValueConst new_target, } } re = js_get_regexp(ctx, pat, FALSE); + flags = JS_UNDEFINED; if (re) { pattern = JS_DupValue(ctx, JS_MKPTR(JS_TAG_STRING, re->pattern)); if (JS_IsUndefined(flags1)) { bc = JS_DupValue(ctx, JS_MKPTR(JS_TAG_STRING, re->bytecode)); + obj = js_create_from_ctor(ctx, new_target, JS_CLASS_REGEXP); + if (JS_IsException(obj)) + goto fail; goto no_compilation; } else { - flags = JS_ToString(ctx, flags1); - if (JS_IsException(flags)) - goto fail; + flags = JS_DupValue(ctx, flags1); } } else { - flags = JS_UNDEFINED; if (pat_is_regexp) { pattern = JS_GetProperty(ctx, pat, JS_ATOM_source); if (JS_IsException(pattern)) @@ -44273,15 +46646,19 @@ static JSValue js_regexp_constructor(JSContext *ctx, JSValueConst new_target, goto fail; } } + obj = js_create_from_ctor(ctx, new_target, JS_CLASS_REGEXP); + if (JS_IsException(obj)) + goto fail; bc = js_compile_regexp(ctx, pattern, flags); if (JS_IsException(bc)) goto fail; JS_FreeValue(ctx, flags); no_compilation: - return js_regexp_constructor_internal(ctx, new_target, pattern, bc); + return js_regexp_set_internal(ctx, obj, pattern, bc); fail: JS_FreeValue(ctx, pattern); JS_FreeValue(ctx, flags); + JS_FreeValue(ctx, obj); return JS_EXCEPTION; } @@ -44436,49 +46813,34 @@ static JSValue js_regexp_get_flag(JSContext *ctx, JSValueConst this_val, int mas return JS_NewBool(ctx, flags & mask); } +#define RE_FLAG_COUNT 8 + static JSValue js_regexp_get_flags(JSContext *ctx, JSValueConst this_val) { - char str[8], *p = str; - int res; - + char str[RE_FLAG_COUNT], *p = str; + int res, i; + static const int flag_atom[RE_FLAG_COUNT] = { + JS_ATOM_hasIndices, + JS_ATOM_global, + JS_ATOM_ignoreCase, + JS_ATOM_multiline, + JS_ATOM_dotAll, + JS_ATOM_unicode, + JS_ATOM_unicodeSets, + JS_ATOM_sticky, + }; + static const char flag_char[RE_FLAG_COUNT] = { 'd', 'g', 'i', 'm', 's', 'u', 'v', 'y' }; + if (JS_VALUE_GET_TAG(this_val) != JS_TAG_OBJECT) return JS_ThrowTypeErrorNotAnObject(ctx); - res = JS_ToBoolFree(ctx, JS_GetPropertyStr(ctx, this_val, "hasIndices")); - if (res < 0) - goto exception; - if (res) - *p++ = 'd'; - res = JS_ToBoolFree(ctx, JS_GetProperty(ctx, this_val, JS_ATOM_global)); - if (res < 0) - goto exception; - if (res) - *p++ = 'g'; - res = JS_ToBoolFree(ctx, JS_GetPropertyStr(ctx, this_val, "ignoreCase")); - if (res < 0) - goto exception; - if (res) - *p++ = 'i'; - res = JS_ToBoolFree(ctx, JS_GetPropertyStr(ctx, this_val, "multiline")); - if (res < 0) - goto exception; - if (res) - *p++ = 'm'; - res = JS_ToBoolFree(ctx, JS_GetPropertyStr(ctx, this_val, "dotAll")); - if (res < 0) - goto exception; - if (res) - *p++ = 's'; - res = JS_ToBoolFree(ctx, JS_GetProperty(ctx, this_val, JS_ATOM_unicode)); - if (res < 0) - goto exception; - if (res) - *p++ = 'u'; - res = JS_ToBoolFree(ctx, JS_GetPropertyStr(ctx, this_val, "sticky")); - if (res < 0) - goto exception; - if (res) - *p++ = 'y'; + for(i = 0; i < RE_FLAG_COUNT; i++) { + res = JS_ToBoolFree(ctx, JS_GetProperty(ctx, this_val, flag_atom[i])); + if (res < 0) + goto exception; + if (res) + *p++ = flag_char[i]; + } return JS_NewStringLen(ctx, str, p - str); exception: @@ -44531,6 +46893,58 @@ void *lre_realloc(void *opaque, void *ptr, size_t size) return js_realloc_rt(ctx->rt, ptr, size); } +static JSValue js_regexp_escape(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue str; + StringBuffer b_s, *b = &b_s; + JSString *p; + uint32_t c, i; + char s[16]; + + if (!JS_IsString(argv[0])) + return JS_ThrowTypeError(ctx, "not a string"); + str = JS_ToString(ctx, argv[0]); /* must call it to linearlize ropes */ + if (JS_IsException(str)) + return JS_EXCEPTION; + p = JS_VALUE_GET_STRING(str); + string_buffer_init2(ctx, b, 0, p->is_wide_char); + for (i = 0; i < p->len; i++) { + c = string_get(p, i); + if (c < 33) { + if (c >= 9 && c <= 13) { + string_buffer_putc8(b, '\\'); + string_buffer_putc8(b, "tnvfr"[c - 9]); + } else { + goto hex2; + } + } else if (c < 128) { + if ((c >= '0' && c <= '9') + || (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z')) { + if (i == 0) + goto hex2; + } else if (strchr(",-=<>#&!%:;@~'`\"", c)) { + goto hex2; + } else if (c != '_') { + string_buffer_putc8(b, '\\'); + } + string_buffer_putc8(b, c); + } else if (c < 256) { + hex2: + snprintf(s, sizeof(s), "\\x%02x", c); + string_buffer_puts8(b, s); + } else if (is_surrogate(c) || lre_is_space(c)) { + snprintf(s, sizeof(s), "\\u%04x", c); + string_buffer_puts8(b, s); + } else { + string_buffer_putc16(b, c); + } + } + JS_FreeValue(ctx, str); + return string_buffer_end(b); +} + static JSValue js_regexp_exec(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -44911,14 +47325,12 @@ static JSValue js_regexp_Symbol_match(JSContext *ctx, JSValueConst this_val, goto exception; p = JS_VALUE_GET_STRING(flags); - // TODO(bnoordhuis) query 'u' flag the same way? global = (-1 != string_indexof_char(p, 'g', 0)); if (!global) { A = JS_RegExpExec(ctx, rx, S); } else { - fullUnicode = JS_ToBoolFree(ctx, JS_GetProperty(ctx, rx, JS_ATOM_unicode)); - if (fullUnicode < 0) - goto exception; + fullUnicode = (string_indexof_char(p, 'u', 0) >= 0 || + string_indexof_char(p, 'v', 0) >= 0); if (JS_SetProperty(ctx, rx, JS_ATOM_lastIndex, JS_NewInt32(ctx, 0)) < 0) goto exception; @@ -44937,7 +47349,7 @@ static JSValue js_regexp_Symbol_match(JSContext *ctx, JSValueConst this_val, if (JS_IsException(matchStr)) goto exception; isEmpty = JS_IsEmptyString(matchStr); - if (JS_SetPropertyInt64(ctx, A, n++, matchStr) < 0) + if (JS_DefinePropertyValueInt64(ctx, A, n++, matchStr, JS_PROP_C_W_E | JS_PROP_THROW) < 0) goto exception; if (isEmpty) { int64_t thisIndex, nextIndex; @@ -45102,7 +47514,8 @@ static JSValue js_regexp_Symbol_matchAll(JSContext *ctx, JSValueConst this_val, it->iterated_string = S; strp = JS_VALUE_GET_STRING(flags); it->global = string_indexof_char(strp, 'g', 0) >= 0; - it->unicode = string_indexof_char(strp, 'u', 0) >= 0; + it->unicode = (string_indexof_char(strp, 'u', 0) >= 0 || + string_indexof_char(strp, 'v', 0) >= 0); it->done = FALSE; JS_SetOpaque(iter, it); @@ -45249,13 +47662,11 @@ static JSValue js_regexp_Symbol_replace(JSContext *ctx, JSValueConst this_val, goto exception; p = JS_VALUE_GET_STRING(flags); - // TODO(bnoordhuis) query 'u' flag the same way? fullUnicode = 0; is_global = (-1 != string_indexof_char(p, 'g', 0)); if (is_global) { - fullUnicode = JS_ToBoolFree(ctx, JS_GetProperty(ctx, rx, JS_ATOM_unicode)); - if (fullUnicode < 0) - goto exception; + fullUnicode = (string_indexof_char(p, 'u', 0) >= 0 || + string_indexof_char(p, 'v', 0) >= 0); if (JS_SetProperty(ctx, rx, JS_ATOM_lastIndex, JS_NewInt32(ctx, 0)) < 0) goto exception; } @@ -45481,7 +47892,8 @@ static JSValue js_regexp_Symbol_split(JSContext *ctx, JSValueConst this_val, if (JS_IsException(flags)) goto exception; strp = JS_VALUE_GET_STRING(flags); - unicodeMatching = string_indexof_char(strp, 'u', 0) >= 0; + unicodeMatching = (string_indexof_char(strp, 'u', 0) >= 0 || + string_indexof_char(strp, 'v', 0) >= 0); if (string_indexof_char(strp, 'y', 0) < 0) { flags = JS_ConcatString3(ctx, "", flags, "y"); if (JS_IsException(flags)) @@ -45578,6 +47990,7 @@ static JSValue js_regexp_Symbol_split(JSContext *ctx, JSValueConst this_val, } static const JSCFunctionListEntry js_regexp_funcs[] = { + JS_CFUNC_DEF("escape", 1, js_regexp_escape ), JS_CGETSET_DEF("[Symbol.species]", js_get_this, NULL ), //JS_CFUNC_DEF("__RegExpExec", 2, js_regexp___RegExpExec ), //JS_CFUNC_DEF("__RegExpDelete", 2, js_regexp___RegExpDelete ), @@ -45591,6 +48004,7 @@ static const JSCFunctionListEntry js_regexp_proto_funcs[] = { JS_CGETSET_MAGIC_DEF("multiline", js_regexp_get_flag, NULL, LRE_FLAG_MULTILINE ), JS_CGETSET_MAGIC_DEF("dotAll", js_regexp_get_flag, NULL, LRE_FLAG_DOTALL ), JS_CGETSET_MAGIC_DEF("unicode", js_regexp_get_flag, NULL, LRE_FLAG_UNICODE ), + JS_CGETSET_MAGIC_DEF("unicodeSets", js_regexp_get_flag, NULL, LRE_FLAG_UNICODE_SETS ), JS_CGETSET_MAGIC_DEF("sticky", js_regexp_get_flag, NULL, LRE_FLAG_STICKY ), JS_CGETSET_MAGIC_DEF("hasIndices", js_regexp_get_flag, NULL, LRE_FLAG_INDICES ), JS_CFUNC_DEF("exec", 1, js_regexp_exec ), @@ -45616,25 +48030,29 @@ void JS_AddIntrinsicRegExpCompiler(JSContext *ctx) ctx->compile_regexp = js_compile_regexp; } -void JS_AddIntrinsicRegExp(JSContext *ctx) +int JS_AddIntrinsicRegExp(JSContext *ctx) { - JSValueConst obj; + JSValue obj; JS_AddIntrinsicRegExpCompiler(ctx); - ctx->class_proto[JS_CLASS_REGEXP] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_REGEXP], js_regexp_proto_funcs, - countof(js_regexp_proto_funcs)); - obj = JS_NewGlobalCConstructor(ctx, "RegExp", js_regexp_constructor, 2, - ctx->class_proto[JS_CLASS_REGEXP]); - ctx->regexp_ctor = JS_DupValue(ctx, obj); - JS_SetPropertyFunctionList(ctx, obj, js_regexp_funcs, countof(js_regexp_funcs)); - + obj = JS_NewCConstructor(ctx, JS_CLASS_REGEXP, "RegExp", + js_regexp_constructor, 2, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_regexp_funcs, countof(js_regexp_funcs), + js_regexp_proto_funcs, countof(js_regexp_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; + ctx->regexp_ctor = obj; + ctx->class_proto[JS_CLASS_REGEXP_STRING_ITERATOR] = - JS_NewObjectProto(ctx, ctx->iterator_proto); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_REGEXP_STRING_ITERATOR], - js_regexp_string_iterator_proto_funcs, - countof(js_regexp_string_iterator_proto_funcs)); + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + js_regexp_string_iterator_proto_funcs, + countof(js_regexp_string_iterator_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_REGEXP_STRING_ITERATOR])) + return -1; + return 0; } /* JSON */ @@ -45753,6 +48171,12 @@ static JSValue json_parse_value(JSParseState *s) val = JS_NewBool(ctx, s->token.u.ident.atom == JS_ATOM_true); } else if (s->token.u.ident.atom == JS_ATOM_null) { val = JS_NULL; + } else if (s->token.u.ident.atom == JS_ATOM_NaN && s->ext_json) { + /* Note: json5 identifier handling is ambiguous e.g. is + '{ NaN: 1 }' a valid JSON5 production ? */ + val = JS_NewFloat64(s->ctx, NAN); + } else if (s->token.u.ident.atom == JS_ATOM_Infinity && s->ext_json) { + val = JS_NewFloat64(s->ctx, INFINITY); } else { goto def_token; } @@ -45857,7 +48281,7 @@ static JSValue internalize_json_property(JSContext *ctx, JSValueConst holder, goto fail; } } - js_free_prop_enum(ctx, atoms, len); + JS_FreePropertyEnum(ctx, atoms, len); atoms = NULL; name_val = JS_AtomToValue(ctx, name); if (JS_IsException(name_val)) @@ -45869,7 +48293,7 @@ static JSValue internalize_json_property(JSContext *ctx, JSValueConst holder, JS_FreeValue(ctx, val); return res; fail: - js_free_prop_enum(ctx, atoms, len); + JS_FreePropertyEnum(ctx, atoms, len); JS_FreeValue(ctx, val); return JS_EXCEPTION; } @@ -45917,10 +48341,72 @@ typedef struct JSONStringifyContext { StringBuffer *b; } JSONStringifyContext; -static JSValue JS_ToQuotedStringFree(JSContext *ctx, JSValue val) { - JSValue r = JS_ToQuotedString(ctx, val); +static int JS_ToQuotedString(JSContext *ctx, StringBuffer *b, JSValueConst val1) +{ + JSValue val; + JSString *p; + int i; + uint32_t c; + char buf[16]; + + val = JS_ToStringCheckObject(ctx, val1); + if (JS_IsException(val)) + return -1; + p = JS_VALUE_GET_STRING(val); + + if (string_buffer_putc8(b, '\"')) + goto fail; + for(i = 0; i < p->len; ) { + c = string_getc(p, &i); + switch(c) { + case '\t': + c = 't'; + goto quote; + case '\r': + c = 'r'; + goto quote; + case '\n': + c = 'n'; + goto quote; + case '\b': + c = 'b'; + goto quote; + case '\f': + c = 'f'; + goto quote; + case '\"': + case '\\': + quote: + if (string_buffer_putc8(b, '\\')) + goto fail; + if (string_buffer_putc8(b, c)) + goto fail; + break; + default: + if (c < 32 || is_surrogate(c)) { + snprintf(buf, sizeof(buf), "\\u%04x", c); + if (string_buffer_puts8(b, buf)) + goto fail; + } else { + if (string_buffer_putc(b, c)) + goto fail; + } + break; + } + } + if (string_buffer_putc8(b, '\"')) + goto fail; + JS_FreeValue(ctx, val); + return 0; + fail: + JS_FreeValue(ctx, val); + return -1; +} + +static int JS_ToQuotedStringFree(JSContext *ctx, StringBuffer *b, JSValue val) { + int ret = JS_ToQuotedString(ctx, b, val); JS_FreeValue(ctx, val); - return r; + return ret; } static JSValue js_json_check(JSContext *ctx, JSONStringifyContext *jsc, @@ -46103,13 +48589,11 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, if (!JS_IsUndefined(v)) { if (has_content) string_buffer_putc8(jsc->b, ','); - prop = JS_ToQuotedStringFree(ctx, prop); - if (JS_IsException(prop)) { + string_buffer_concat_value(jsc->b, sep); + if (JS_ToQuotedString(ctx, jsc->b, prop)) { JS_FreeValue(ctx, v); goto exception; } - string_buffer_concat_value(jsc->b, sep); - string_buffer_concat_value(jsc->b, prop); string_buffer_putc8(jsc->b, ':'); string_buffer_concat_value(jsc->b, sep1); if (js_json_to_str(ctx, jsc, val, v, indent1)) @@ -46137,10 +48621,7 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, switch (JS_VALUE_GET_NORM_TAG(val)) { case JS_TAG_STRING: case JS_TAG_STRING_ROPE: - val = JS_ToQuotedStringFree(ctx, val); - if (JS_IsException(val)) - goto exception; - goto concat_value; + return JS_ToQuotedStringFree(ctx, jsc->b, val); case JS_TAG_FLOAT64: if (!isfinite(JS_VALUE_GET_FLOAT64(val))) { val = JS_NULL; @@ -46322,10 +48803,10 @@ static const JSCFunctionListEntry js_json_obj[] = { JS_OBJECT_DEF("JSON", js_json_funcs, countof(js_json_funcs), JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE ), }; -void JS_AddIntrinsicJSON(JSContext *ctx) +int JS_AddIntrinsicJSON(JSContext *ctx) { /* add JSON as autoinit object */ - JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_json_obj, countof(js_json_obj)); + return JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_json_obj, countof(js_json_obj)); } /* Reflect */ @@ -46348,7 +48829,7 @@ static JSValue js_reflect_construct(JSContext *ctx, JSValueConst this_val, if (argc > 2) { new_target = argv[2]; if (!JS_IsConstructor(ctx, new_target)) - return JS_ThrowTypeError(ctx, "not a constructor"); + return JS_ThrowTypeErrorNotAConstructor(ctx, new_target); } else { new_target = func; } @@ -46579,7 +49060,7 @@ static JSValue js_proxy_get_prototype(JSContext *ctx, JSValueConst obj) JS_FreeValue(ctx, ret); return JS_EXCEPTION; } - if (JS_VALUE_GET_OBJ(proto1) != JS_VALUE_GET_OBJ(ret)) { + if (!js_same_value(ctx, proto1, ret)) { JS_FreeValue(ctx, proto1); fail: JS_FreeValue(ctx, ret); @@ -46619,7 +49100,7 @@ static int js_proxy_set_prototype(JSContext *ctx, JSValueConst obj, proto1 = JS_GetPrototype(ctx, s->target); if (JS_IsException(proto1)) return -1; - if (JS_VALUE_GET_OBJ(proto_val) != JS_VALUE_GET_OBJ(proto1)) { + if (!js_same_value(ctx, proto_val, proto1)) { JS_FreeValue(ctx, proto1); JS_ThrowTypeError(ctx, "proxy: inconsistent prototype"); return -1; @@ -47243,14 +49724,14 @@ static int js_proxy_get_own_property_names(JSContext *ctx, } } - js_free_prop_enum(ctx, tab2, len2); + JS_FreePropertyEnum(ctx, tab2, len2); JS_FreeValue(ctx, prop_array); *ptab = tab; *plen = len; return 0; fail: - js_free_prop_enum(ctx, tab2, len2); - js_free_prop_enum(ctx, tab, len); + JS_FreePropertyEnum(ctx, tab2, len2); + JS_FreePropertyEnum(ctx, tab, len); JS_FreeValue(ctx, prop_array); return -1; } @@ -47267,7 +49748,7 @@ static JSValue js_proxy_call_constructor(JSContext *ctx, JSValueConst func_obj, if (!s) return JS_EXCEPTION; if (!JS_IsConstructor(ctx, s->target)) - return JS_ThrowTypeError(ctx, "not a constructor"); + return JS_ThrowTypeErrorNotAConstructor(ctx, s->target); if (JS_IsUndefined(method)) return JS_CallConstructor2(ctx, s->target, new_target, argc, argv); arg_array = js_create_array(ctx, argc, argv); @@ -47452,25 +49933,36 @@ static const JSClassShortDef js_proxy_class_def[] = { { JS_ATOM_Object, js_proxy_finalizer, js_proxy_mark }, /* JS_CLASS_PROXY */ }; -void JS_AddIntrinsicProxy(JSContext *ctx) +int JS_AddIntrinsicProxy(JSContext *ctx) { JSRuntime *rt = ctx->rt; JSValue obj1; if (!JS_IsRegisteredClass(rt, JS_CLASS_PROXY)) { - init_class_range(rt, js_proxy_class_def, JS_CLASS_PROXY, - countof(js_proxy_class_def)); + if (init_class_range(rt, js_proxy_class_def, JS_CLASS_PROXY, + countof(js_proxy_class_def))) + return -1; rt->class_array[JS_CLASS_PROXY].exotic = &js_proxy_exotic_methods; rt->class_array[JS_CLASS_PROXY].call = js_proxy_call; } - obj1 = JS_NewCFunction2(ctx, js_proxy_constructor, "Proxy", 2, - JS_CFUNC_constructor, 0); + /* additional fields: name, length */ + obj1 = JS_NewCFunction3(ctx, js_proxy_constructor, "Proxy", 2, + JS_CFUNC_constructor, 0, + ctx->function_proto, countof(js_proxy_funcs) + 2); + if (JS_IsException(obj1)) + return -1; JS_SetConstructorBit(ctx, obj1, TRUE); - JS_SetPropertyFunctionList(ctx, obj1, js_proxy_funcs, - countof(js_proxy_funcs)); - JS_DefinePropertyValueStr(ctx, ctx->global_obj, "Proxy", - obj1, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + if (JS_SetPropertyFunctionList(ctx, obj1, js_proxy_funcs, + countof(js_proxy_funcs))) + goto fail; + if (JS_DefinePropertyValueStr(ctx, ctx->global_obj, "Proxy", + obj1, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0) + goto fail; + return 0; + fail: + JS_FreeValue(ctx, obj1); + return -1; } /* Symbol */ @@ -47482,7 +49974,7 @@ static JSValue js_symbol_constructor(JSContext *ctx, JSValueConst new_target, JSString *p; if (!JS_IsUndefined(new_target)) - return JS_ThrowTypeError(ctx, "not a constructor"); + return JS_ThrowTypeErrorNotAConstructor(ctx, new_target); if (argc == 0 || JS_IsUndefined(argv[0])) { p = NULL; } else { @@ -47582,6 +50074,19 @@ static JSValue js_symbol_keyFor(JSContext *ctx, JSValueConst this_val, static const JSCFunctionListEntry js_symbol_funcs[] = { JS_CFUNC_DEF("for", 1, js_symbol_for ), JS_CFUNC_DEF("keyFor", 1, js_symbol_keyFor ), + JS_PROP_ATOM_DEF("toPrimitive", JS_ATOM_Symbol_toPrimitive, 0), + JS_PROP_ATOM_DEF("iterator", JS_ATOM_Symbol_iterator, 0), + JS_PROP_ATOM_DEF("match", JS_ATOM_Symbol_match, 0), + JS_PROP_ATOM_DEF("matchAll", JS_ATOM_Symbol_matchAll, 0), + JS_PROP_ATOM_DEF("replace", JS_ATOM_Symbol_replace, 0), + JS_PROP_ATOM_DEF("search", JS_ATOM_Symbol_search, 0), + JS_PROP_ATOM_DEF("split", JS_ATOM_Symbol_split, 0), + JS_PROP_ATOM_DEF("toStringTag", JS_ATOM_Symbol_toStringTag, 0), + JS_PROP_ATOM_DEF("isConcatSpreadable", JS_ATOM_Symbol_isConcatSpreadable, 0), + JS_PROP_ATOM_DEF("hasInstance", JS_ATOM_Symbol_hasInstance, 0), + JS_PROP_ATOM_DEF("species", JS_ATOM_Symbol_species, 0), + JS_PROP_ATOM_DEF("unscopables", JS_ATOM_Symbol_unscopables, 0), + JS_PROP_ATOM_DEF("asyncIterator", JS_ATOM_Symbol_asyncIterator, 0), }; /* Set/Map/WeakSet/WeakMap */ @@ -47774,7 +50279,7 @@ static JSValue js_map_constructor(JSContext *ctx, JSValueConst new_target, } /* XXX: could normalize strings to speed up comparison */ -static JSValueConst map_normalize_key(JSContext *ctx, JSValueConst key) +static JSValue map_normalize_key(JSContext *ctx, JSValue key) { uint32_t tag = JS_VALUE_GET_TAG(key); /* convert -0.0 to +0.0 */ @@ -47784,6 +50289,11 @@ static JSValueConst map_normalize_key(JSContext *ctx, JSValueConst key) return key; } +static JSValueConst map_normalize_key_const(JSContext *ctx, JSValueConst key) +{ + return (JSValueConst)map_normalize_key(ctx, (JSValue)key); +} + /* hash multipliers, same as the Linux kernel (see Knuth vol 3, section 6.4, exercise 9) */ #define HASH_MUL32 0x61C88647 @@ -47943,8 +50453,19 @@ static JSMapRecord *map_add_record(JSContext *ctx, JSMapState *s, return mr; } +static JSMapRecord *set_add_record(JSContext *ctx, JSMapState *s, + JSValueConst key) +{ + JSMapRecord *mr; + mr = map_add_record(ctx, s, key); + if (!mr) + return NULL; + mr->value = JS_UNDEFINED; + return mr; +} + /* warning: the record must be removed from the hash table before */ -static void map_delete_record(JSRuntime *rt, JSMapState *s, JSMapRecord *mr) +static void map_delete_record_internal(JSRuntime *rt, JSMapState *s, JSMapRecord *mr) { if (mr->empty) return; @@ -48004,7 +50525,7 @@ static void map_delete_weakrefs(JSRuntime *rt, JSWeakRefHeader *wh) /* remove from the hash table */ *pmr = mr1->hash_next; done: - map_delete_record(rt, s, mr); + map_delete_record_internal(rt, s, mr); } } } @@ -48018,7 +50539,7 @@ static JSValue js_map_set(JSContext *ctx, JSValueConst this_val, if (!s) return JS_EXCEPTION; - key = map_normalize_key(ctx, argv[0]); + key = map_normalize_key_const(ctx, argv[0]); if (s->is_weak && !js_weakref_is_target(key)) return JS_ThrowTypeError(ctx, "invalid value used as %s key", (magic & MAGIC_SET) ? "WeakSet" : "WeakMap"); if (magic & MAGIC_SET) @@ -48046,7 +50567,7 @@ static JSValue js_map_get(JSContext *ctx, JSValueConst this_val, if (!s) return JS_EXCEPTION; - key = map_normalize_key(ctx, argv[0]); + key = map_normalize_key_const(ctx, argv[0]); mr = map_find_record(ctx, s, key); if (!mr) return JS_UNDEFINED; @@ -48054,31 +50575,13 @@ static JSValue js_map_get(JSContext *ctx, JSValueConst this_val, return JS_DupValue(ctx, mr->value); } -static JSValue js_map_has(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv, int magic) -{ - JSMapState *s = JS_GetOpaque2(ctx, this_val, JS_CLASS_MAP + magic); - JSMapRecord *mr; - JSValueConst key; - - if (!s) - return JS_EXCEPTION; - key = map_normalize_key(ctx, argv[0]); - mr = map_find_record(ctx, s, key); - return JS_NewBool(ctx, mr != NULL); -} - -static JSValue js_map_delete(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv, int magic) +/* return JS_TRUE or JS_FALSE */ +static JSValue map_delete_record(JSContext *ctx, JSMapState *s, JSValueConst key) { - JSMapState *s = JS_GetOpaque2(ctx, this_val, JS_CLASS_MAP + magic); JSMapRecord *mr, **pmr; - JSValueConst key; uint32_t h; - if (!s) - return JS_EXCEPTION; - key = map_normalize_key(ctx, argv[0]); + key = map_normalize_key_const(ctx, key); h = map_hash_key(key, s->hash_bits); pmr = &s->hash_table[h]; @@ -48098,10 +50601,70 @@ static JSValue js_map_delete(JSContext *ctx, JSValueConst this_val, /* remove from the hash table */ *pmr = mr->hash_next; - map_delete_record(ctx->rt, s, mr); + map_delete_record_internal(ctx->rt, s, mr); return JS_TRUE; } +static JSValue js_map_getOrInsert(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + BOOL computed = magic & 1; + JSClassID class_id = magic >> 1; + JSMapState *s = JS_GetOpaque2(ctx, this_val, class_id); + JSMapRecord *mr; + JSValueConst key; + JSValue value; + + if (!s) + return JS_EXCEPTION; + if (computed && !JS_IsFunction(ctx, argv[1])) + return JS_ThrowTypeError(ctx, "not a function"); + key = map_normalize_key_const(ctx, argv[0]); + if (s->is_weak && !js_weakref_is_target(key)) + return JS_ThrowTypeError(ctx, "invalid value used as WeakMap key"); + mr = map_find_record(ctx, s, key); + if (!mr) { + if (computed) { + value = JS_Call(ctx, argv[1], JS_UNDEFINED, 1, &key); + if (JS_IsException(value)) + return JS_EXCEPTION; + map_delete_record(ctx, s, key); + } else { + value = JS_DupValue(ctx, argv[1]); + } + mr = map_add_record(ctx, s, key); + if (!mr) { + JS_FreeValue(ctx, value); + return JS_EXCEPTION; + } + mr->value = value; + } + return JS_DupValue(ctx, mr->value); +} + +static JSValue js_map_has(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSMapState *s = JS_GetOpaque2(ctx, this_val, JS_CLASS_MAP + magic); + JSMapRecord *mr; + JSValueConst key; + + if (!s) + return JS_EXCEPTION; + key = map_normalize_key_const(ctx, argv[0]); + mr = map_find_record(ctx, s, key); + return JS_NewBool(ctx, mr != NULL); +} + +static JSValue js_map_delete(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSMapState *s = JS_GetOpaque2(ctx, this_val, JS_CLASS_MAP + magic); + if (!s) + return JS_EXCEPTION; + return map_delete_record(ctx, s, argv[0]); +} + static JSValue js_map_clear(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { @@ -48117,7 +50680,7 @@ static JSValue js_map_clear(JSContext *ctx, JSValueConst this_val, list_for_each_safe(el, el1, &s->records) { mr = list_entry(el, JSMapRecord, link); - map_delete_record(ctx->rt, s, mr); + map_delete_record_internal(ctx->rt, s, mr); } return JS_UNDEFINED; } @@ -48477,6 +51040,560 @@ static JSValue js_map_iterator_next(JSContext *ctx, JSValueConst this_val, } } +static int get_set_record(JSContext *ctx, JSValueConst obj, + int64_t *psize, JSValue *phas, JSValue *pkeys) +{ + JSMapState *s; + int64_t size; + JSValue has = JS_UNDEFINED, keys = JS_UNDEFINED; + + s = JS_GetOpaque(obj, JS_CLASS_SET); + if (s) { + size = s->record_count; + } else { + JSValue v; + double d; + + v = JS_GetProperty(ctx, obj, JS_ATOM_size); + if (JS_IsException(v)) + goto exception; + if (JS_ToFloat64Free(ctx, &d, v) < 0) + goto exception; + if (isnan(d)) { + JS_ThrowTypeError(ctx, ".size is not a number"); + goto exception; + } + if (d < INT64_MIN) + size = INT64_MIN; + else if (d >= 0x1p63) /* must use INT64_MAX + 1 because INT64_MAX cannot be exactly represented as a double */ + size = INT64_MAX; + else + size = (int64_t)d; + if (size < 0) { + JS_ThrowRangeError(ctx, ".size must be positive"); + goto exception; + } + } + + has = JS_GetProperty(ctx, obj, JS_ATOM_has); + if (JS_IsException(has)) + goto exception; + if (JS_IsUndefined(has)) { + JS_ThrowTypeError(ctx, ".has is undefined"); + goto exception; + } + if (!JS_IsFunction(ctx, has)) { + JS_ThrowTypeError(ctx, ".has is not a function"); + goto exception; + } + + keys = JS_GetProperty(ctx, obj, JS_ATOM_keys); + if (JS_IsException(keys)) + goto exception; + if (JS_IsUndefined(keys)) { + JS_ThrowTypeError(ctx, ".keys is undefined"); + goto exception; + } + if (!JS_IsFunction(ctx, keys)) { + JS_ThrowTypeError(ctx, ".keys is not a function"); + goto exception; + } + *psize = size; + *phas = has; + *pkeys = keys; + return 0; + + exception: + JS_FreeValue(ctx, has); + JS_FreeValue(ctx, keys); + *psize = 0; + *phas = JS_UNDEFINED; + *pkeys = JS_UNDEFINED; + return -1; +} + +/* copy 'this_val' in a new set without side effects */ +static JSValue js_copy_set(JSContext *ctx, JSValueConst this_val) +{ + JSValue newset; + JSMapState *s, *t; + struct list_head *el; + JSMapRecord *mr; + + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + + newset = js_map_constructor(ctx, JS_UNDEFINED, 0, NULL, MAGIC_SET); + if (JS_IsException(newset)) + return JS_EXCEPTION; + t = JS_GetOpaque(newset, JS_CLASS_SET); + + // can't clone this_val using js_map_constructor(), + // test262 mandates we don't call the .add method + list_for_each(el, &s->records) { + mr = list_entry(el, JSMapRecord, link); + if (mr->empty) + continue; + if (!set_add_record(ctx, t, mr->key)) + goto exception; + } + return newset; + exception: + JS_FreeValue(ctx, newset); + return JS_EXCEPTION; +} + +static JSValue js_set_isDisjointFrom(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue item, iter, keys, has, next, rv, rval; + int done; + BOOL found; + JSMapState *s; + int64_t size; + int ok; + + iter = JS_UNDEFINED; + next = JS_UNDEFINED; + rval = JS_EXCEPTION; + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + if (get_set_record(ctx, argv[0], &size, &has, &keys) < 0) + goto exception; + if (s->record_count <= size) { + iter = js_create_map_iterator(ctx, this_val, 0, NULL, MAGIC_SET); + if (JS_IsException(iter)) + goto exception; + found = FALSE; + do { + item = js_map_iterator_next(ctx, iter, 0, NULL, &done, MAGIC_SET); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + rv = JS_Call(ctx, has, argv[0], 1, (JSValueConst *)&item); + JS_FreeValue(ctx, item); + ok = JS_ToBoolFree(ctx, rv); // returns -1 if rv is JS_EXCEPTION + if (ok < 0) + goto exception; + found = (ok > 0); + } while (!found); + } else { + iter = JS_Call(ctx, keys, argv[0], 0, NULL); + if (JS_IsException(iter)) + goto exception; + next = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(next)) + goto exception; + found = FALSE; + for(;;) { + item = JS_IteratorNext(ctx, iter, next, 0, NULL, &done); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + item = map_normalize_key(ctx, item); + found = (NULL != map_find_record(ctx, s, item)); + JS_FreeValue(ctx, item); + if (found) { + JS_IteratorClose(ctx, iter, FALSE); + break; + } + } + } + rval = !found ? JS_TRUE : JS_FALSE; +exception: + JS_FreeValue(ctx, has); + JS_FreeValue(ctx, keys); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, next); + return rval; +} + +static JSValue js_set_isSubsetOf(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue item, iter, keys, has, next, rv, rval; + BOOL found; + JSMapState *s; + int64_t size; + int done, ok; + + iter = JS_UNDEFINED; + next = JS_UNDEFINED; + rval = JS_EXCEPTION; + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + if (get_set_record(ctx, argv[0], &size, &has, &keys) < 0) + goto exception; + found = FALSE; + if (s->record_count > size) + goto fini; + iter = js_create_map_iterator(ctx, this_val, 0, NULL, MAGIC_SET); + if (JS_IsException(iter)) + goto exception; + found = TRUE; + do { + item = js_map_iterator_next(ctx, iter, 0, NULL, &done, MAGIC_SET); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + rv = JS_Call(ctx, has, argv[0], 1, (JSValueConst *)&item); + JS_FreeValue(ctx, item); + ok = JS_ToBoolFree(ctx, rv); // returns -1 if rv is JS_EXCEPTION + if (ok < 0) + goto exception; + found = (ok > 0); + } while (found); +fini: + rval = found ? JS_TRUE : JS_FALSE; +exception: + JS_FreeValue(ctx, has); + JS_FreeValue(ctx, keys); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, next); + return rval; +} + +static JSValue js_set_isSupersetOf(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue item, iter, keys, has, next, rval; + int done; + BOOL found; + JSMapState *s; + int64_t size; + + iter = JS_UNDEFINED; + next = JS_UNDEFINED; + rval = JS_EXCEPTION; + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + if (get_set_record(ctx, argv[0], &size, &has, &keys) < 0) + goto exception; + found = FALSE; + if (s->record_count < size) + goto fini; + iter = JS_Call(ctx, keys, argv[0], 0, NULL); + if (JS_IsException(iter)) + goto exception; + next = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(next)) + goto exception; + found = TRUE; + for(;;) { + item = JS_IteratorNext(ctx, iter, next, 0, NULL, &done); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + item = map_normalize_key(ctx, item); + found = (NULL != map_find_record(ctx, s, item)); + JS_FreeValue(ctx, item); + if (!found) { + JS_IteratorClose(ctx, iter, FALSE); + break; + } + } +fini: + rval = found ? JS_TRUE : JS_FALSE; +exception: + JS_FreeValue(ctx, has); + JS_FreeValue(ctx, keys); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, next); + return rval; +} + +static JSValue js_set_intersection(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue newset, item, iter, keys, has, next, rv; + JSMapState *s, *t; + JSMapRecord *mr; + int64_t size; + int done, ok; + + iter = JS_UNDEFINED; + next = JS_UNDEFINED; + newset = JS_UNDEFINED; + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + if (get_set_record(ctx, argv[0], &size, &has, &keys) < 0) + goto exception; + if (s->record_count > size) { + iter = JS_Call(ctx, keys, argv[0], 0, NULL); + if (JS_IsException(iter)) + goto exception; + next = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(next)) + goto exception; + newset = js_map_constructor(ctx, JS_UNDEFINED, 0, NULL, MAGIC_SET); + if (JS_IsException(newset)) + goto exception; + t = JS_GetOpaque(newset, JS_CLASS_SET); + for (;;) { + item = JS_IteratorNext(ctx, iter, next, 0, NULL, &done); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + item = map_normalize_key(ctx, item); + if (!map_find_record(ctx, s, item)) { + JS_FreeValue(ctx, item); + } else if (map_find_record(ctx, t, item)) { + JS_FreeValue(ctx, item); // no duplicates + } else { + mr = set_add_record(ctx, t, item); + JS_FreeValue(ctx, item); + if (!mr) + goto exception; + } + } + } else { + iter = js_create_map_iterator(ctx, this_val, 0, NULL, MAGIC_SET); + if (JS_IsException(iter)) + goto exception; + newset = js_map_constructor(ctx, JS_UNDEFINED, 0, NULL, MAGIC_SET); + if (JS_IsException(newset)) + goto exception; + t = JS_GetOpaque(newset, JS_CLASS_SET); + for (;;) { + item = js_map_iterator_next(ctx, iter, 0, NULL, &done, MAGIC_SET); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + rv = JS_Call(ctx, has, argv[0], 1, (JSValueConst *)&item); + ok = JS_ToBoolFree(ctx, rv); // returns -1 if rv is JS_EXCEPTION + if (ok > 0) { + item = map_normalize_key(ctx, item); + if (map_find_record(ctx, t, item)) { + JS_FreeValue(ctx, item); // no duplicates + } else { + mr = set_add_record(ctx, t, item); + JS_FreeValue(ctx, item); + if (!mr) + goto exception; + } + } else { + JS_FreeValue(ctx, item); + if (ok < 0) + goto exception; + } + } + } + goto fini; +exception: + JS_FreeValue(ctx, newset); + newset = JS_EXCEPTION; +fini: + JS_FreeValue(ctx, has); + JS_FreeValue(ctx, keys); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, next); + return newset; +} + +static JSValue js_set_difference(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue newset, item, iter, keys, has, next, rv; + JSMapState *s, *t; + int64_t size; + int done; + int ok; + + iter = JS_UNDEFINED; + next = JS_UNDEFINED; + newset = JS_UNDEFINED; + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + if (get_set_record(ctx, argv[0], &size, &has, &keys) < 0) + goto exception; + + newset = js_copy_set(ctx, this_val); + if (JS_IsException(newset)) + goto exception; + t = JS_GetOpaque(newset, JS_CLASS_SET); + + if (s->record_count <= size) { + iter = js_create_map_iterator(ctx, newset, 0, NULL, MAGIC_SET); + if (JS_IsException(iter)) + goto exception; + for (;;) { + item = js_map_iterator_next(ctx, iter, 0, NULL, &done, MAGIC_SET); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + rv = JS_Call(ctx, has, argv[0], 1, (JSValueConst *)&item); + ok = JS_ToBoolFree(ctx, rv); // returns -1 if rv is JS_EXCEPTION + if (ok < 0) { + JS_FreeValue(ctx, item); + goto exception; + } + if (ok) { + map_delete_record(ctx, t, item); + } + JS_FreeValue(ctx, item); + } + } else { + iter = JS_Call(ctx, keys, argv[0], 0, NULL); + if (JS_IsException(iter)) + goto exception; + next = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(next)) + goto exception; + for (;;) { + item = JS_IteratorNext(ctx, iter, next, 0, NULL, &done); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + map_delete_record(ctx, t, item); + JS_FreeValue(ctx, item); + } + } + goto fini; +exception: + JS_FreeValue(ctx, newset); + newset = JS_EXCEPTION; +fini: + JS_FreeValue(ctx, has); + JS_FreeValue(ctx, keys); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, next); + return newset; +} + +static JSValue js_set_symmetricDifference(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue newset, item, iter, next, has, keys; + JSMapState *s, *t; + JSMapRecord *mr; + int64_t size; + int done; + BOOL present; + + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + if (get_set_record(ctx, argv[0], &size, &has, &keys) < 0) + return JS_EXCEPTION; + JS_FreeValue(ctx, has); + + next = JS_UNDEFINED; + newset = JS_UNDEFINED; + iter = JS_Call(ctx, keys, argv[0], 0, NULL); + if (JS_IsException(iter)) + goto exception; + next = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(next)) + goto exception; + newset = js_copy_set(ctx, this_val); + if (JS_IsException(newset)) + goto exception; + t = JS_GetOpaque(newset, JS_CLASS_SET); + for (;;) { + item = JS_IteratorNext(ctx, iter, next, 0, NULL, &done); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + // note the subtlety here: due to mutating iterators, it's + // possible for keys to disappear during iteration; test262 + // still expects us to maintain insertion order though, so + // we first check |this|, then |new|; |new| is a copy of |this| + // - if item exists in |this|, delete (if it exists) from |new| + // - if item misses in |this| and |new|, add to |new| + // - if item exists in |new| but misses in |this|, *don't* add it, + // mutating iterator erased it + item = map_normalize_key(ctx, item); + present = (NULL != map_find_record(ctx, s, item)); + mr = map_find_record(ctx, t, item); + if (present) { + map_delete_record(ctx, t, item); + JS_FreeValue(ctx, item); + } else if (mr) { + JS_FreeValue(ctx, item); + } else { + mr = set_add_record(ctx, t, item); + JS_FreeValue(ctx, item); + if (!mr) + goto exception; + } + } + goto fini; +exception: + JS_FreeValue(ctx, newset); + newset = JS_EXCEPTION; +fini: + JS_FreeValue(ctx, next); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, keys); + return newset; +} + +static JSValue js_set_union(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue newset, item, iter, next, has, keys, rv; + JSMapState *s; + int64_t size; + int done; + + s = JS_GetOpaque2(ctx, this_val, JS_CLASS_SET); + if (!s) + return JS_EXCEPTION; + if (get_set_record(ctx, argv[0], &size, &has, &keys) < 0) + return JS_EXCEPTION; + JS_FreeValue(ctx, has); + + next = JS_UNDEFINED; + newset = JS_UNDEFINED; + iter = JS_Call(ctx, keys, argv[0], 0, NULL); + if (JS_IsException(iter)) + goto exception; + next = JS_GetProperty(ctx, iter, JS_ATOM_next); + if (JS_IsException(next)) + goto exception; + + newset = js_copy_set(ctx, this_val); + if (JS_IsException(newset)) + goto exception; + + for (;;) { + item = JS_IteratorNext(ctx, iter, next, 0, NULL, &done); + if (JS_IsException(item)) + goto exception; + if (done) // item is JS_UNDEFINED + break; + rv = js_map_set(ctx, newset, 1, (JSValueConst *)&item, MAGIC_SET); + JS_FreeValue(ctx, item); + if (JS_IsException(rv)) + goto exception; + JS_FreeValue(ctx, rv); + } + goto fini; +exception: + JS_FreeValue(ctx, newset); + newset = JS_EXCEPTION; +fini: + JS_FreeValue(ctx, next); + JS_FreeValue(ctx, iter); + JS_FreeValue(ctx, keys); + return newset; +} + static const JSCFunctionListEntry js_map_funcs[] = { JS_CFUNC_MAGIC_DEF("groupBy", 2, js_object_groupBy, 1 ), JS_CGETSET_DEF("[Symbol.species]", js_get_this, NULL ), @@ -48485,6 +51602,10 @@ static const JSCFunctionListEntry js_map_funcs[] = { static const JSCFunctionListEntry js_map_proto_funcs[] = { JS_CFUNC_MAGIC_DEF("set", 2, js_map_set, 0 ), JS_CFUNC_MAGIC_DEF("get", 1, js_map_get, 0 ), + JS_CFUNC_MAGIC_DEF("getOrInsert", 2, js_map_getOrInsert, + (JS_CLASS_MAP << 1) | /*computed*/FALSE ), + JS_CFUNC_MAGIC_DEF("getOrInsertComputed", 2, js_map_getOrInsert, + (JS_CLASS_MAP << 1) | /*computed*/TRUE ), JS_CFUNC_MAGIC_DEF("has", 1, js_map_has, 0 ), JS_CFUNC_MAGIC_DEF("delete", 1, js_map_delete, 0 ), JS_CFUNC_MAGIC_DEF("clear", 0, js_map_clear, 0 ), @@ -48509,6 +51630,13 @@ static const JSCFunctionListEntry js_set_proto_funcs[] = { JS_CFUNC_MAGIC_DEF("clear", 0, js_map_clear, MAGIC_SET ), JS_CGETSET_MAGIC_DEF("size", js_map_get_size, NULL, MAGIC_SET ), JS_CFUNC_MAGIC_DEF("forEach", 1, js_map_forEach, MAGIC_SET ), + JS_CFUNC_DEF("isDisjointFrom", 1, js_set_isDisjointFrom ), + JS_CFUNC_DEF("isSubsetOf", 1, js_set_isSubsetOf ), + JS_CFUNC_DEF("isSupersetOf", 1, js_set_isSupersetOf ), + JS_CFUNC_DEF("intersection", 1, js_set_intersection ), + JS_CFUNC_DEF("difference", 1, js_set_difference ), + JS_CFUNC_DEF("symmetricDifference", 1, js_set_symmetricDifference ), + JS_CFUNC_DEF("union", 1, js_set_union ), JS_CFUNC_MAGIC_DEF("values", 0, js_create_map_iterator, (JS_ITERATOR_KIND_KEY << 2) | MAGIC_SET ), JS_ALIAS_DEF("keys", "values" ), JS_ALIAS_DEF("[Symbol.iterator]", "values" ), @@ -48524,6 +51652,10 @@ static const JSCFunctionListEntry js_set_iterator_proto_funcs[] = { static const JSCFunctionListEntry js_weak_map_proto_funcs[] = { JS_CFUNC_MAGIC_DEF("set", 2, js_map_set, MAGIC_WEAK ), JS_CFUNC_MAGIC_DEF("get", 1, js_map_get, MAGIC_WEAK ), + JS_CFUNC_MAGIC_DEF("getOrInsert", 2, js_map_getOrInsert, + (JS_CLASS_WEAKMAP << 1) | /*computed*/FALSE ), + JS_CFUNC_MAGIC_DEF("getOrInsertComputed", 2, js_map_getOrInsert, + (JS_CLASS_WEAKMAP << 1) | /*computed*/TRUE ), JS_CFUNC_MAGIC_DEF("has", 1, js_map_has, MAGIC_WEAK ), JS_CFUNC_MAGIC_DEF("delete", 1, js_map_delete, MAGIC_WEAK ), JS_PROP_STRING_DEF("[Symbol.toStringTag]", "WeakMap", JS_PROP_CONFIGURABLE ), @@ -48554,35 +51686,37 @@ static const uint8_t js_map_proto_funcs_count[6] = { countof(js_set_iterator_proto_funcs), }; -void JS_AddIntrinsicMapSet(JSContext *ctx) +int JS_AddIntrinsicMapSet(JSContext *ctx) { int i; JSValue obj1; char buf[ATOM_GET_STR_BUF_SIZE]; for(i = 0; i < 4; i++) { + JSCFunctionType ft; const char *name = JS_AtomGetStr(ctx, buf, sizeof(buf), JS_ATOM_Map + i); - ctx->class_proto[JS_CLASS_MAP + i] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_MAP + i], - js_map_proto_funcs_ptr[i], - js_map_proto_funcs_count[i]); - obj1 = JS_NewCFunctionMagic(ctx, js_map_constructor, name, 0, - JS_CFUNC_constructor_magic, i); - if (i < 2) { - JS_SetPropertyFunctionList(ctx, obj1, js_map_funcs, - countof(js_map_funcs)); - } - JS_NewGlobalCConstructor2(ctx, obj1, name, ctx->class_proto[JS_CLASS_MAP + i]); + ft.constructor_magic = js_map_constructor; + obj1 = JS_NewCConstructor(ctx, JS_CLASS_MAP + i, name, + ft.generic, 0, JS_CFUNC_constructor_magic, i, + JS_UNDEFINED, + js_map_funcs, i < 2 ? countof(js_map_funcs) : 0, + js_map_proto_funcs_ptr[i], js_map_proto_funcs_count[i], + 0); + if (JS_IsException(obj1)) + return -1; + JS_FreeValue(ctx, obj1); } for(i = 0; i < 2; i++) { ctx->class_proto[JS_CLASS_MAP_ITERATOR + i] = - JS_NewObjectProto(ctx, ctx->iterator_proto); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_MAP_ITERATOR + i], - js_map_proto_funcs_ptr[i + 4], - js_map_proto_funcs_count[i + 4]); + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + js_map_proto_funcs_ptr[i + 4], + js_map_proto_funcs_count[i + 4]); + if (JS_IsException(ctx->class_proto[JS_CLASS_MAP_ITERATOR + i])) + return -1; } + return 0; } /* Generator */ @@ -49095,16 +52229,57 @@ static JSValue js_promise_withResolvers(JSContext *ctx, if (JS_IsException(result_promise)) return result_promise; obj = JS_NewObject(ctx); - if (JS_IsException(obj)) { - JS_FreeValue(ctx, resolving_funcs[0]); - JS_FreeValue(ctx, resolving_funcs[1]); - JS_FreeValue(ctx, result_promise); - return JS_EXCEPTION; + if (JS_IsException(obj)) + goto exception; + if (JS_DefinePropertyValue(ctx, obj, JS_ATOM_promise, result_promise, + JS_PROP_C_W_E) < 0) { + goto exception; + } + result_promise = JS_UNDEFINED; + if (JS_DefinePropertyValue(ctx, obj, JS_ATOM_resolve, resolving_funcs[0], + JS_PROP_C_W_E) < 0) { + goto exception; + } + resolving_funcs[0] = JS_UNDEFINED; + if (JS_DefinePropertyValue(ctx, obj, JS_ATOM_reject, resolving_funcs[1], + JS_PROP_C_W_E) < 0) { + goto exception; } - JS_DefinePropertyValue(ctx, obj, JS_ATOM_promise, result_promise, JS_PROP_C_W_E); - JS_DefinePropertyValue(ctx, obj, JS_ATOM_resolve, resolving_funcs[0], JS_PROP_C_W_E); - JS_DefinePropertyValue(ctx, obj, JS_ATOM_reject, resolving_funcs[1], JS_PROP_C_W_E); return obj; +exception: + JS_FreeValue(ctx, resolving_funcs[0]); + JS_FreeValue(ctx, resolving_funcs[1]); + JS_FreeValue(ctx, result_promise); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; +} + +static JSValue js_promise_try(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue result_promise, resolving_funcs[2], ret, ret2; + BOOL is_reject = 0; + + if (!JS_IsObject(this_val)) + return JS_ThrowTypeErrorNotAnObject(ctx); + result_promise = js_new_promise_capability(ctx, resolving_funcs, this_val); + if (JS_IsException(result_promise)) + return result_promise; + ret = JS_Call(ctx, argv[0], JS_UNDEFINED, argc - 1, argv + 1); + if (JS_IsException(ret)) { + is_reject = 1; + ret = JS_GetException(ctx); + } + ret2 = JS_Call(ctx, resolving_funcs[is_reject], JS_UNDEFINED, 1, (JSValueConst *)&ret); + JS_FreeValue(ctx, resolving_funcs[0]); + JS_FreeValue(ctx, resolving_funcs[1]); + JS_FreeValue(ctx, ret); + if (JS_IsException(ret2)) { + JS_FreeValue(ctx, result_promise); + return ret2; + } + JS_FreeValue(ctx, ret2); + return result_promise; } static __exception int remainingElementsCount_add(JSContext *ctx, @@ -49594,6 +52769,7 @@ static const JSCFunctionListEntry js_promise_funcs[] = { JS_CFUNC_MAGIC_DEF("all", 1, js_promise_all, PROMISE_MAGIC_all ), JS_CFUNC_MAGIC_DEF("allSettled", 1, js_promise_all, PROMISE_MAGIC_allSettled ), JS_CFUNC_MAGIC_DEF("any", 1, js_promise_all, PROMISE_MAGIC_any ), + JS_CFUNC_DEF("try", 1, js_promise_try ), JS_CFUNC_DEF("race", 1, js_promise_race ), JS_CFUNC_DEF("withResolvers", 0, js_promise_withResolvers ), JS_CGETSET_DEF("[Symbol.species]", js_get_this, NULL), @@ -49859,14 +53035,16 @@ static JSClassShortDef const js_async_class_def[] = { { JS_ATOM_AsyncGenerator, js_async_generator_finalizer, js_async_generator_mark }, /* JS_CLASS_ASYNC_GENERATOR */ }; -void JS_AddIntrinsicPromise(JSContext *ctx) +int JS_AddIntrinsicPromise(JSContext *ctx) { JSRuntime *rt = ctx->rt; JSValue obj1; + JSCFunctionType ft; if (!JS_IsRegisteredClass(rt, JS_CLASS_PROMISE)) { - init_class_range(rt, js_async_class_def, JS_CLASS_PROMISE, - countof(js_async_class_def)); + if (init_class_range(rt, js_async_class_def, JS_CLASS_PROMISE, + countof(js_async_class_def))) + return -1; rt->class_array[JS_CLASS_PROMISE_RESOLVE_FUNCTION].call = js_promise_resolve_function_call; rt->class_array[JS_CLASS_PROMISE_REJECT_FUNCTION].call = js_promise_resolve_function_call; rt->class_array[JS_CLASS_ASYNC_FUNCTION].call = js_async_function_call; @@ -49876,72 +53054,67 @@ void JS_AddIntrinsicPromise(JSContext *ctx) } /* Promise */ - ctx->class_proto[JS_CLASS_PROMISE] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_PROMISE], - js_promise_proto_funcs, - countof(js_promise_proto_funcs)); - obj1 = JS_NewCFunction2(ctx, js_promise_constructor, "Promise", 1, - JS_CFUNC_constructor, 0); - ctx->promise_ctor = JS_DupValue(ctx, obj1); - JS_SetPropertyFunctionList(ctx, obj1, - js_promise_funcs, - countof(js_promise_funcs)); - JS_NewGlobalCConstructor2(ctx, obj1, "Promise", - ctx->class_proto[JS_CLASS_PROMISE]); - + obj1 = JS_NewCConstructor(ctx, JS_CLASS_PROMISE, "Promise", + js_promise_constructor, 1, JS_CFUNC_constructor, 0, + JS_UNDEFINED, + js_promise_funcs, countof(js_promise_funcs), + js_promise_proto_funcs, countof(js_promise_proto_funcs), + 0); + if (JS_IsException(obj1)) + return -1; + ctx->promise_ctor = obj1; + /* AsyncFunction */ - ctx->class_proto[JS_CLASS_ASYNC_FUNCTION] = JS_NewObjectProto(ctx, ctx->function_proto); - obj1 = JS_NewCFunction3(ctx, (JSCFunction *)js_function_constructor, - "AsyncFunction", 1, - JS_CFUNC_constructor_or_func_magic, JS_FUNC_ASYNC, - ctx->function_ctor); - JS_SetPropertyFunctionList(ctx, - ctx->class_proto[JS_CLASS_ASYNC_FUNCTION], - js_async_function_proto_funcs, - countof(js_async_function_proto_funcs)); - JS_SetConstructor2(ctx, obj1, ctx->class_proto[JS_CLASS_ASYNC_FUNCTION], - 0, JS_PROP_CONFIGURABLE); + ft.generic_magic = js_function_constructor; + obj1 = JS_NewCConstructor(ctx, JS_CLASS_ASYNC_FUNCTION, "AsyncFunction", + ft.generic, 1, JS_CFUNC_constructor_or_func_magic, JS_FUNC_ASYNC, + ctx->function_ctor, + NULL, 0, + js_async_function_proto_funcs, countof(js_async_function_proto_funcs), + JS_NEW_CTOR_NO_GLOBAL | JS_NEW_CTOR_READONLY); + if (JS_IsException(obj1)) + return -1; JS_FreeValue(ctx, obj1); - + /* AsyncIteratorPrototype */ - ctx->async_iterator_proto = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->async_iterator_proto, - js_async_iterator_proto_funcs, - countof(js_async_iterator_proto_funcs)); + ctx->async_iterator_proto = + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_OBJECT], + js_async_iterator_proto_funcs, + countof(js_async_iterator_proto_funcs)); + if (JS_IsException(ctx->async_iterator_proto)) + return -1; /* AsyncFromSyncIteratorPrototype */ ctx->class_proto[JS_CLASS_ASYNC_FROM_SYNC_ITERATOR] = - JS_NewObjectProto(ctx, ctx->async_iterator_proto); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_ASYNC_FROM_SYNC_ITERATOR], - js_async_from_sync_iterator_proto_funcs, - countof(js_async_from_sync_iterator_proto_funcs)); - + JS_NewObjectProtoList(ctx, ctx->async_iterator_proto, + js_async_from_sync_iterator_proto_funcs, + countof(js_async_from_sync_iterator_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_ASYNC_FROM_SYNC_ITERATOR])) + return -1; + /* AsyncGeneratorPrototype */ ctx->class_proto[JS_CLASS_ASYNC_GENERATOR] = - JS_NewObjectProto(ctx, ctx->async_iterator_proto); - JS_SetPropertyFunctionList(ctx, - ctx->class_proto[JS_CLASS_ASYNC_GENERATOR], - js_async_generator_proto_funcs, - countof(js_async_generator_proto_funcs)); + JS_NewObjectProtoList(ctx, ctx->async_iterator_proto, + js_async_generator_proto_funcs, + countof(js_async_generator_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_ASYNC_GENERATOR])) + return -1; /* AsyncGeneratorFunction */ - ctx->class_proto[JS_CLASS_ASYNC_GENERATOR_FUNCTION] = - JS_NewObjectProto(ctx, ctx->function_proto); - obj1 = JS_NewCFunction3(ctx, (JSCFunction *)js_function_constructor, - "AsyncGeneratorFunction", 1, - JS_CFUNC_constructor_or_func_magic, - JS_FUNC_ASYNC_GENERATOR, - ctx->function_ctor); - JS_SetPropertyFunctionList(ctx, - ctx->class_proto[JS_CLASS_ASYNC_GENERATOR_FUNCTION], - js_async_generator_function_proto_funcs, - countof(js_async_generator_function_proto_funcs)); - JS_SetConstructor2(ctx, ctx->class_proto[JS_CLASS_ASYNC_GENERATOR_FUNCTION], - ctx->class_proto[JS_CLASS_ASYNC_GENERATOR], - JS_PROP_CONFIGURABLE, JS_PROP_CONFIGURABLE); - JS_SetConstructor2(ctx, obj1, ctx->class_proto[JS_CLASS_ASYNC_GENERATOR_FUNCTION], - 0, JS_PROP_CONFIGURABLE); + ft.generic_magic = js_function_constructor; + obj1 = JS_NewCConstructor(ctx, JS_CLASS_ASYNC_GENERATOR_FUNCTION, "AsyncGeneratorFunction", + ft.generic, 1, JS_CFUNC_constructor_or_func_magic, JS_FUNC_ASYNC_GENERATOR, + ctx->function_ctor, + NULL, 0, + js_async_generator_function_proto_funcs, countof(js_async_generator_function_proto_funcs), + JS_NEW_CTOR_NO_GLOBAL | JS_NEW_CTOR_READONLY); + if (JS_IsException(obj1)) + return -1; JS_FreeValue(ctx, obj1); + + return JS_SetConstructor2(ctx, ctx->class_proto[JS_CLASS_ASYNC_GENERATOR_FUNCTION], + ctx->class_proto[JS_CLASS_ASYNC_GENERATOR], + JS_PROP_CONFIGURABLE, JS_PROP_CONFIGURABLE); } /* URI handling */ @@ -50236,6 +53409,7 @@ static const JSCFunctionListEntry js_global_funcs[] = { JS_PROP_DOUBLE_DEF("NaN", NAN, 0 ), JS_PROP_UNDEFINED_DEF("undefined", 0 ), JS_PROP_STRING_DEF("[Symbol.toStringTag]", "global", JS_PROP_CONFIGURABLE ), + JS_CFUNC_DEF("eval", 1, js_global_eval ), }; /* Date */ @@ -50434,6 +53608,21 @@ static double set_date_fields(double fields[minimum_length(7)], int is_local) { return time_clip(tv); } +static double set_date_fields_checked(double fields[minimum_length(7)], int is_local) +{ + int i; + double a; + for(i = 0; i < 7; i++) { + a = fields[i]; + if (!isfinite(a)) + return NAN; + fields[i] = trunc(a); + if (i == 0 && fields[0] >= 0 && fields[0] < 100) + fields[0] += 1900; + } + return set_date_fields(fields, is_local); +} + static JSValue get_date_field(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { @@ -50619,7 +53808,7 @@ static JSValue js_date_constructor(JSContext *ctx, JSValueConst new_target, // Date(y, mon, d, h, m, s, ms) JSValue rv; int i, n; - double a, val; + double val; if (JS_IsUndefined(new_target)) { /* invoked as function */ @@ -50657,15 +53846,10 @@ static JSValue js_date_constructor(JSContext *ctx, JSValueConst new_target, if (n > 7) n = 7; for(i = 0; i < n; i++) { - if (JS_ToFloat64(ctx, &a, argv[i])) + if (JS_ToFloat64(ctx, &fields[i], argv[i])) return JS_EXCEPTION; - if (!isfinite(a)) - break; - fields[i] = trunc(a); - if (i == 0 && fields[0] >= 0 && fields[0] < 100) - fields[0] += 1900; } - val = (i == n) ? set_date_fields(fields, 1) : NAN; + val = set_date_fields_checked(fields, 1); } has_val: #if 0 @@ -50695,7 +53879,6 @@ static JSValue js_Date_UTC(JSContext *ctx, JSValueConst this_val, // UTC(y, mon, d, h, m, s, ms) double fields[] = { 0, 0, 1, 0, 0, 0, 0 }; int i, n; - double a; n = argc; if (n == 0) @@ -50703,15 +53886,10 @@ static JSValue js_Date_UTC(JSContext *ctx, JSValueConst this_val, if (n > 7) n = 7; for(i = 0; i < n; i++) { - if (JS_ToFloat64(ctx, &a, argv[i])) + if (JS_ToFloat64(ctx, &fields[i], argv[i])) return JS_EXCEPTION; - if (!isfinite(a)) - return JS_NAN; - fields[i] = trunc(a); - if (i == 0 && fields[0] >= 0 && fields[0] < 100) - fields[0] += 1900; } - return JS_NewFloat64(ctx, set_date_fields(fields, 0)); + return JS_NewFloat64(ctx, set_date_fields_checked(fields, 0)); } /* Date string parsing */ @@ -50822,9 +54000,14 @@ static BOOL string_get_tzoffset(const uint8_t *sp, int *pp, int *tzp, BOOL stric hh = hh / 100; } else { mm = 0; - if (string_skip_char(sp, &p, ':') /* optional separator */ - && !string_get_digits(sp, &p, &mm, 2, 2)) - return FALSE; + if (string_skip_char(sp, &p, ':')) { + /* optional separator */ + if (!string_get_digits(sp, &p, &mm, 2, 2)) + return FALSE; + } else { + if (strict) + return FALSE; /* [+-]HH is not accepted in strict mode */ + } } if (hh > 23 || mm > 59) return FALSE; @@ -51023,12 +54206,16 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, string_get_milliseconds(sp, &p, &fields[6]); } has_time = TRUE; + if ((sp[p] == '+' || sp[p] == '-') && + string_get_tzoffset(sp, &p, &fields[8], FALSE)) { + *is_local = FALSE; + } } else { - if (p - p_start > 2) { + if (p - p_start > 2 && !has_year) { fields[0] = val; has_year = TRUE; } else - if (val < 1 || val > 31) { + if ((val < 1 || val > 31) && !has_year) { fields[0] = val + (val < 100) * 1900 + (val < 50) * 100; has_year = TRUE; } else { @@ -51367,24 +54554,29 @@ JSValue JS_NewDate(JSContext *ctx, double epoch_ms) return obj; } -void JS_AddIntrinsicDate(JSContext *ctx) +int JS_AddIntrinsicDate(JSContext *ctx) { - JSValueConst obj; + JSValue obj; /* Date */ - ctx->class_proto[JS_CLASS_DATE] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_DATE], js_date_proto_funcs, - countof(js_date_proto_funcs)); - obj = JS_NewGlobalCConstructor(ctx, "Date", js_date_constructor, 7, - ctx->class_proto[JS_CLASS_DATE]); - JS_SetPropertyFunctionList(ctx, obj, js_date_funcs, countof(js_date_funcs)); + obj = JS_NewCConstructor(ctx, JS_CLASS_DATE, "Date", + js_date_constructor, 7, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_date_funcs, countof(js_date_funcs), + js_date_proto_funcs, countof(js_date_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; + JS_FreeValue(ctx, obj); + return 0; } /* eval */ -void JS_AddIntrinsicEval(JSContext *ctx) +int JS_AddIntrinsicEval(JSContext *ctx) { ctx->eval_internal = __JS_EvalInternal; + return 0; } /* BigInt */ @@ -51444,7 +54636,7 @@ static JSValue js_bigint_constructor(JSContext *ctx, int argc, JSValueConst *argv) { if (!JS_IsUndefined(new_target)) - return JS_ThrowTypeError(ctx, "not a constructor"); + return JS_ThrowTypeErrorNotAConstructor(ctx, new_target); return JS_ToBigIntCtorFree(ctx, JS_DupValue(ctx, argv[0])); } @@ -51568,311 +54760,350 @@ static const JSCFunctionListEntry js_bigint_proto_funcs[] = { JS_PROP_STRING_DEF("[Symbol.toStringTag]", "BigInt", JS_PROP_CONFIGURABLE ), }; -static void JS_AddIntrinsicBigInt(JSContext *ctx) +static int JS_AddIntrinsicBigInt(JSContext *ctx) { - JSValueConst obj1; + JSValue obj1; - ctx->class_proto[JS_CLASS_BIG_INT] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_BIG_INT], - js_bigint_proto_funcs, - countof(js_bigint_proto_funcs)); - obj1 = JS_NewGlobalCConstructor(ctx, "BigInt", js_bigint_constructor, 1, - ctx->class_proto[JS_CLASS_BIG_INT]); - JS_SetPropertyFunctionList(ctx, obj1, js_bigint_funcs, - countof(js_bigint_funcs)); + obj1 = JS_NewCConstructor(ctx, JS_CLASS_BIG_INT, "BigInt", + js_bigint_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_bigint_funcs, countof(js_bigint_funcs), + js_bigint_proto_funcs, countof(js_bigint_proto_funcs), + 0); + if (JS_IsException(obj1)) + return -1; + JS_FreeValue(ctx, obj1); + return 0; } -static const char * const native_error_name[JS_NATIVE_ERROR_COUNT] = { - "EvalError", "RangeError", "ReferenceError", - "SyntaxError", "TypeError", "URIError", - "InternalError", "AggregateError", -}; - /* Minimum amount of objects to be able to compile code and display - error messages. No JSAtom should be allocated by this function. */ -static void JS_AddIntrinsicBasicObjects(JSContext *ctx) + error messages. */ +static int JS_AddIntrinsicBasicObjects(JSContext *ctx) { - JSValue proto; + JSValue obj; + JSCFunctionType ft; int i; - ctx->class_proto[JS_CLASS_OBJECT] = JS_NewObjectProto(ctx, JS_NULL); + /* warning: ordering is tricky */ + ctx->class_proto[JS_CLASS_OBJECT] = + JS_NewObjectProtoClassAlloc(ctx, JS_NULL, JS_CLASS_OBJECT, + countof(js_object_proto_funcs) + 1); + if (JS_IsException(ctx->class_proto[JS_CLASS_OBJECT])) + return -1; JS_SetImmutablePrototype(ctx, ctx->class_proto[JS_CLASS_OBJECT]); - + + /* 2 more properties: caller and arguments */ ctx->function_proto = JS_NewCFunction3(ctx, js_function_proto, "", 0, JS_CFUNC_generic, 0, - ctx->class_proto[JS_CLASS_OBJECT]); + ctx->class_proto[JS_CLASS_OBJECT], + countof(js_function_proto_funcs) + 3 + 2); + if (JS_IsException(ctx->function_proto)) + return -1; ctx->class_proto[JS_CLASS_BYTECODE_FUNCTION] = JS_DupValue(ctx, ctx->function_proto); - ctx->class_proto[JS_CLASS_ERROR] = JS_NewObject(ctx); -#if 0 - /* these are auto-initialized from js_error_proto_funcs, - but delaying might be a problem */ - JS_DefinePropertyValue(ctx, ctx->class_proto[JS_CLASS_ERROR], JS_ATOM_name, - JS_AtomToString(ctx, JS_ATOM_Error), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - JS_DefinePropertyValue(ctx, ctx->class_proto[JS_CLASS_ERROR], JS_ATOM_message, - JS_AtomToString(ctx, JS_ATOM_empty_string), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); -#endif - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_ERROR], - js_error_proto_funcs, - countof(js_error_proto_funcs)); + + ctx->global_obj = JS_NewObjectProtoClassAlloc(ctx, ctx->class_proto[JS_CLASS_OBJECT], + JS_CLASS_GLOBAL_OBJECT, 64); + if (JS_IsException(ctx->global_obj)) + return -1; + { + JSObject *p; + obj = JS_NewObjectProtoClassAlloc(ctx, JS_NULL, JS_CLASS_OBJECT, 4); + p = JS_VALUE_GET_OBJ(ctx->global_obj); + p->u.global_object.uninitialized_vars = obj; + } + ctx->global_var_obj = JS_NewObjectProtoClassAlloc(ctx, JS_NULL, + JS_CLASS_OBJECT, 16); + if (JS_IsException(ctx->global_var_obj)) + return -1; + + /* Error */ + ft.generic_magic = js_error_constructor; + obj = JS_NewCConstructor(ctx, JS_CLASS_ERROR, "Error", + ft.generic, 1, JS_CFUNC_constructor_or_func_magic, -1, + JS_UNDEFINED, + js_error_funcs, countof(js_error_funcs), + js_error_proto_funcs, countof(js_error_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; for(i = 0; i < JS_NATIVE_ERROR_COUNT; i++) { - proto = JS_NewObjectProto(ctx, ctx->class_proto[JS_CLASS_ERROR]); - JS_DefinePropertyValue(ctx, proto, JS_ATOM_name, - JS_NewAtomString(ctx, native_error_name[i]), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - JS_DefinePropertyValue(ctx, proto, JS_ATOM_message, - JS_AtomToString(ctx, JS_ATOM_empty_string), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - ctx->native_error_proto[i] = proto; + JSValue func_obj; + const JSCFunctionListEntry *funcs; + int n_args; + char buf[ATOM_GET_STR_BUF_SIZE]; + const char *name = JS_AtomGetStr(ctx, buf, sizeof(buf), + JS_ATOM_EvalError + i); + n_args = 1 + (i == JS_AGGREGATE_ERROR); + funcs = js_native_error_proto_funcs + 2 * i; + func_obj = JS_NewCConstructor(ctx, -1, name, + ft.generic, n_args, JS_CFUNC_constructor_or_func_magic, i, + obj, + NULL, 0, + funcs, 2, + 0); + if (JS_IsException(func_obj)) { + JS_FreeValue(ctx, obj); + return -1; + } + ctx->native_error_proto[i] = JS_GetProperty(ctx, func_obj, JS_ATOM_prototype); + JS_FreeValue(ctx, func_obj); + if (JS_IsException(ctx->native_error_proto[i])) { + JS_FreeValue(ctx, obj); + return -1; + } } + JS_FreeValue(ctx, obj); - /* the array prototype is an array */ - ctx->class_proto[JS_CLASS_ARRAY] = - JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], - JS_CLASS_ARRAY); + /* Array */ + obj = JS_NewCConstructor(ctx, JS_CLASS_ARRAY, "Array", + js_array_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_array_funcs, countof(js_array_funcs), + js_array_proto_funcs, countof(js_array_proto_funcs), + JS_NEW_CTOR_PROTO_CLASS); + if (JS_IsException(obj)) + return -1; + ctx->array_ctor = obj; ctx->array_shape = js_new_shape2(ctx, get_proto_obj(ctx->class_proto[JS_CLASS_ARRAY]), JS_PROP_INITIAL_HASH_SIZE, 1); - add_shape_property(ctx, &ctx->array_shape, NULL, - JS_ATOM_length, JS_PROP_WRITABLE | JS_PROP_LENGTH); - - /* XXX: could test it on first context creation to ensure that no - new atoms are created in JS_AddIntrinsicBasicObjects(). It is - necessary to avoid useless renumbering of atoms after - JS_EvalBinary() if it is done just after - JS_AddIntrinsicBasicObjects(). */ - // assert(ctx->rt->atom_count == JS_ATOM_END); + if (!ctx->array_shape) + return -1; + if (add_shape_property(ctx, &ctx->array_shape, NULL, + JS_ATOM_length, JS_PROP_WRITABLE | JS_PROP_LENGTH)) + return -1; + ctx->std_array_prototype = TRUE; + + return 0; } -void JS_AddIntrinsicBaseObjects(JSContext *ctx) +int JS_AddIntrinsicBaseObjects(JSContext *ctx) { - int i; - JSValueConst obj, number_obj; - JSValue obj1; + JSValue obj1, obj2; + JSCFunctionType ft; ctx->throw_type_error = JS_NewCFunction(ctx, js_throw_type_error, NULL, 0); - + if (JS_IsException(ctx->throw_type_error)) + return -1; /* add caller and arguments properties to throw a TypeError */ - JS_DefineProperty(ctx, ctx->function_proto, JS_ATOM_caller, JS_UNDEFINED, - ctx->throw_type_error, ctx->throw_type_error, - JS_PROP_HAS_GET | JS_PROP_HAS_SET | - JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE); - JS_DefineProperty(ctx, ctx->function_proto, JS_ATOM_arguments, JS_UNDEFINED, - ctx->throw_type_error, ctx->throw_type_error, - JS_PROP_HAS_GET | JS_PROP_HAS_SET | - JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE); + if (JS_DefineProperty(ctx, ctx->function_proto, JS_ATOM_caller, JS_UNDEFINED, + ctx->throw_type_error, ctx->throw_type_error, + JS_PROP_HAS_GET | JS_PROP_HAS_SET | + JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE) < 0) + return -1; + if (JS_DefineProperty(ctx, ctx->function_proto, JS_ATOM_arguments, JS_UNDEFINED, + ctx->throw_type_error, ctx->throw_type_error, + JS_PROP_HAS_GET | JS_PROP_HAS_SET | + JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE) < 0) + return -1; JS_FreeValue(ctx, js_object_seal(ctx, JS_UNDEFINED, 1, (JSValueConst *)&ctx->throw_type_error, 1)); - ctx->global_obj = JS_NewObject(ctx); - ctx->global_var_obj = JS_NewObjectProto(ctx, JS_NULL); - /* Object */ - obj = JS_NewGlobalCConstructor(ctx, "Object", js_object_constructor, 1, - ctx->class_proto[JS_CLASS_OBJECT]); - JS_SetPropertyFunctionList(ctx, obj, js_object_funcs, countof(js_object_funcs)); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_OBJECT], - js_object_proto_funcs, countof(js_object_proto_funcs)); - + obj1 = JS_NewCConstructor(ctx, JS_CLASS_OBJECT, "Object", + js_object_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_object_funcs, countof(js_object_funcs), + js_object_proto_funcs, countof(js_object_proto_funcs), + JS_NEW_CTOR_PROTO_EXIST); + if (JS_IsException(obj1)) + return -1; + JS_FreeValue(ctx, obj1); + /* Function */ - JS_SetPropertyFunctionList(ctx, ctx->function_proto, js_function_proto_funcs, countof(js_function_proto_funcs)); - ctx->function_ctor = JS_NewCFunctionMagic(ctx, js_function_constructor, - "Function", 1, JS_CFUNC_constructor_or_func_magic, - JS_FUNC_NORMAL); - JS_NewGlobalCConstructor2(ctx, JS_DupValue(ctx, ctx->function_ctor), "Function", - ctx->function_proto); - - /* Error */ - obj1 = JS_NewCFunctionMagic(ctx, js_error_constructor, - "Error", 1, JS_CFUNC_constructor_or_func_magic, -1); - JS_NewGlobalCConstructor2(ctx, obj1, - "Error", ctx->class_proto[JS_CLASS_ERROR]); - - /* Used to squelch a -Wcast-function-type warning. */ - JSCFunctionType ft = { .generic_magic = js_error_constructor }; - for(i = 0; i < JS_NATIVE_ERROR_COUNT; i++) { - JSValue func_obj; - int n_args; - n_args = 1 + (i == JS_AGGREGATE_ERROR); - func_obj = JS_NewCFunction3(ctx, ft.generic, - native_error_name[i], n_args, - JS_CFUNC_constructor_or_func_magic, i, obj1); - JS_NewGlobalCConstructor2(ctx, func_obj, native_error_name[i], - ctx->native_error_proto[i]); + ft.generic_magic = js_function_constructor; + obj1 = JS_NewCConstructor(ctx, JS_CLASS_BYTECODE_FUNCTION, "Function", + ft.generic, 1, JS_CFUNC_constructor_or_func_magic, JS_FUNC_NORMAL, + JS_UNDEFINED, + NULL, 0, + js_function_proto_funcs, countof(js_function_proto_funcs), + JS_NEW_CTOR_PROTO_EXIST); + if (JS_IsException(obj1)) + return -1; + ctx->function_ctor = obj1; + + /* Iterator */ + obj2 = JS_NewCConstructor(ctx, JS_CLASS_ITERATOR, "Iterator", + js_iterator_constructor, 0, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_iterator_funcs, countof(js_iterator_funcs), + js_iterator_proto_funcs, countof(js_iterator_proto_funcs), + 0); + if (JS_IsException(obj2)) + return -1; + // quirk: Iterator.prototype.constructor is an accessor property + // TODO(bnoordhuis) mildly inefficient because JS_NewGlobalCConstructor + // first creates a .constructor value property that we then replace with + // an accessor + obj1 = JS_NewCFunctionData(ctx, js_iterator_constructor_getset, + 0, 0, 1, (JSValueConst *)&obj2); + if (JS_IsException(obj1)) { + JS_FreeValue(ctx, obj2); + return -1; } - - /* Iterator prototype */ - ctx->iterator_proto = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->iterator_proto, - js_iterator_proto_funcs, - countof(js_iterator_proto_funcs)); - - /* Array */ - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_ARRAY], - js_array_proto_funcs, - countof(js_array_proto_funcs)); - - obj = JS_NewGlobalCConstructor(ctx, "Array", js_array_constructor, 1, - ctx->class_proto[JS_CLASS_ARRAY]); - ctx->array_ctor = JS_DupValue(ctx, obj); - JS_SetPropertyFunctionList(ctx, obj, js_array_funcs, - countof(js_array_funcs)); - - /* XXX: create auto_initializer */ - { - /* initialize Array.prototype[Symbol.unscopables] */ - static const char unscopables[] = - "at" "\0" - "copyWithin" "\0" - "entries" "\0" - "fill" "\0" - "find" "\0" - "findIndex" "\0" - "findLast" "\0" - "findLastIndex" "\0" - "flat" "\0" - "flatMap" "\0" - "includes" "\0" - "keys" "\0" - "toReversed" "\0" - "toSorted" "\0" - "toSpliced" "\0" - "values" "\0"; - const char *p = unscopables; - obj1 = JS_NewObjectProto(ctx, JS_NULL); - for(p = unscopables; *p; p += strlen(p) + 1) { - JS_DefinePropertyValueStr(ctx, obj1, p, JS_TRUE, JS_PROP_C_W_E); - } - JS_DefinePropertyValue(ctx, ctx->class_proto[JS_CLASS_ARRAY], - JS_ATOM_Symbol_unscopables, obj1, - JS_PROP_CONFIGURABLE); + if (JS_DefineProperty(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + JS_ATOM_constructor, JS_UNDEFINED, + obj1, obj1, + JS_PROP_HAS_GET | JS_PROP_HAS_SET | JS_PROP_CONFIGURABLE) < 0) { + JS_FreeValue(ctx, obj2); + JS_FreeValue(ctx, obj1); + return -1; } + JS_FreeValue(ctx, obj1); + ctx->iterator_ctor = obj2; + + ctx->class_proto[JS_CLASS_ITERATOR_HELPER] = + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + js_iterator_helper_proto_funcs, + countof(js_iterator_helper_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_ITERATOR_HELPER])) + return -1; + + ctx->class_proto[JS_CLASS_ITERATOR_WRAP] = + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + js_iterator_wrap_proto_funcs, + countof(js_iterator_wrap_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_ITERATOR_WRAP])) + return -1; /* needed to initialize arguments[Symbol.iterator] */ ctx->array_proto_values = JS_GetProperty(ctx, ctx->class_proto[JS_CLASS_ARRAY], JS_ATOM_values); + if (JS_IsException(ctx->array_proto_values)) + return -1; - ctx->class_proto[JS_CLASS_ARRAY_ITERATOR] = JS_NewObjectProto(ctx, ctx->iterator_proto); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_ARRAY_ITERATOR], - js_array_iterator_proto_funcs, - countof(js_array_iterator_proto_funcs)); + ctx->class_proto[JS_CLASS_ARRAY_ITERATOR] = + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + js_array_iterator_proto_funcs, + countof(js_array_iterator_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_ARRAY_ITERATOR])) + return -1; /* parseFloat and parseInteger must be defined before Number because of the Number.parseFloat and Number.parseInteger aliases */ - JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_global_funcs, - countof(js_global_funcs)); + if (JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_global_funcs, + countof(js_global_funcs))) + return -1; /* Number */ - ctx->class_proto[JS_CLASS_NUMBER] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], - JS_CLASS_NUMBER); - JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_NUMBER], JS_NewInt32(ctx, 0)); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_NUMBER], - js_number_proto_funcs, - countof(js_number_proto_funcs)); - number_obj = JS_NewGlobalCConstructor(ctx, "Number", js_number_constructor, 1, - ctx->class_proto[JS_CLASS_NUMBER]); - JS_SetPropertyFunctionList(ctx, number_obj, js_number_funcs, countof(js_number_funcs)); - + obj1 = JS_NewCConstructor(ctx, JS_CLASS_NUMBER, "Number", + js_number_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_number_funcs, countof(js_number_funcs), + js_number_proto_funcs, countof(js_number_proto_funcs), + JS_NEW_CTOR_PROTO_CLASS); + if (JS_IsException(obj1)) + return -1; + JS_FreeValue(ctx, obj1); + if (JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_NUMBER], JS_NewInt32(ctx, 0))) + return -1; + /* Boolean */ - ctx->class_proto[JS_CLASS_BOOLEAN] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], - JS_CLASS_BOOLEAN); - JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_BOOLEAN], JS_NewBool(ctx, FALSE)); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_BOOLEAN], js_boolean_proto_funcs, - countof(js_boolean_proto_funcs)); - JS_NewGlobalCConstructor(ctx, "Boolean", js_boolean_constructor, 1, - ctx->class_proto[JS_CLASS_BOOLEAN]); + obj1 = JS_NewCConstructor(ctx, JS_CLASS_BOOLEAN, "Boolean", + js_boolean_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + NULL, 0, + js_boolean_proto_funcs, countof(js_boolean_proto_funcs), + JS_NEW_CTOR_PROTO_CLASS); + if (JS_IsException(obj1)) + return -1; + JS_FreeValue(ctx, obj1); + if (JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_BOOLEAN], JS_NewBool(ctx, FALSE))) + return -1; /* String */ - ctx->class_proto[JS_CLASS_STRING] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], - JS_CLASS_STRING); - JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_STRING], JS_AtomToString(ctx, JS_ATOM_empty_string)); - obj = JS_NewGlobalCConstructor(ctx, "String", js_string_constructor, 1, - ctx->class_proto[JS_CLASS_STRING]); - JS_SetPropertyFunctionList(ctx, obj, js_string_funcs, - countof(js_string_funcs)); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_STRING], js_string_proto_funcs, - countof(js_string_proto_funcs)); - - ctx->class_proto[JS_CLASS_STRING_ITERATOR] = JS_NewObjectProto(ctx, ctx->iterator_proto); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_STRING_ITERATOR], - js_string_iterator_proto_funcs, - countof(js_string_iterator_proto_funcs)); + obj1 = JS_NewCConstructor(ctx, JS_CLASS_STRING, "String", + js_string_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_string_funcs, countof(js_string_funcs), + js_string_proto_funcs, countof(js_string_proto_funcs), + JS_NEW_CTOR_PROTO_CLASS); + if (JS_IsException(obj1)) + return -1; + JS_FreeValue(ctx, obj1); + if (JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_STRING], JS_AtomToString(ctx, JS_ATOM_empty_string))) + return -1; + + ctx->class_proto[JS_CLASS_STRING_ITERATOR] = + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + js_string_iterator_proto_funcs, + countof(js_string_iterator_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_STRING_ITERATOR])) + return -1; /* Math: create as autoinit object */ js_random_init(ctx); - JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_math_obj, countof(js_math_obj)); + if (JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_math_obj, countof(js_math_obj))) + return -1; /* ES6 Reflect: create as autoinit object */ - JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_reflect_obj, countof(js_reflect_obj)); + if (JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_reflect_obj, countof(js_reflect_obj))) + return -1; /* ES6 Symbol */ - ctx->class_proto[JS_CLASS_SYMBOL] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_SYMBOL], js_symbol_proto_funcs, - countof(js_symbol_proto_funcs)); - obj = JS_NewGlobalCConstructor(ctx, "Symbol", js_symbol_constructor, 0, - ctx->class_proto[JS_CLASS_SYMBOL]); - JS_SetPropertyFunctionList(ctx, obj, js_symbol_funcs, - countof(js_symbol_funcs)); - for(i = JS_ATOM_Symbol_toPrimitive; i < JS_ATOM_END; i++) { - char buf[ATOM_GET_STR_BUF_SIZE]; - const char *str, *p; - str = JS_AtomGetStr(ctx, buf, sizeof(buf), i); - /* skip "Symbol." */ - p = strchr(str, '.'); - if (p) - str = p + 1; - JS_DefinePropertyValueStr(ctx, obj, str, JS_AtomToValue(ctx, i), 0); - } - - /* ES6 Generator */ - ctx->class_proto[JS_CLASS_GENERATOR] = JS_NewObjectProto(ctx, ctx->iterator_proto); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_GENERATOR], - js_generator_proto_funcs, - countof(js_generator_proto_funcs)); - - ctx->class_proto[JS_CLASS_GENERATOR_FUNCTION] = JS_NewObjectProto(ctx, ctx->function_proto); - obj1 = JS_NewCFunction3(ctx, (JSCFunction *)js_function_constructor, - "GeneratorFunction", 1, - JS_CFUNC_constructor_or_func_magic, JS_FUNC_GENERATOR, - ctx->function_ctor); - JS_SetPropertyFunctionList(ctx, - ctx->class_proto[JS_CLASS_GENERATOR_FUNCTION], - js_generator_function_proto_funcs, - countof(js_generator_function_proto_funcs)); - JS_SetConstructor2(ctx, ctx->class_proto[JS_CLASS_GENERATOR_FUNCTION], - ctx->class_proto[JS_CLASS_GENERATOR], - JS_PROP_CONFIGURABLE, JS_PROP_CONFIGURABLE); - JS_SetConstructor2(ctx, obj1, ctx->class_proto[JS_CLASS_GENERATOR_FUNCTION], - 0, JS_PROP_CONFIGURABLE); + obj1 = JS_NewCConstructor(ctx, JS_CLASS_SYMBOL, "Symbol", + js_symbol_constructor, 0, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_symbol_funcs, countof(js_symbol_funcs), + js_symbol_proto_funcs, countof(js_symbol_proto_funcs), + 0); + if (JS_IsException(obj1)) + return -1; JS_FreeValue(ctx, obj1); + + /* ES6 Generator */ + ctx->class_proto[JS_CLASS_GENERATOR] = + JS_NewObjectProtoList(ctx, ctx->class_proto[JS_CLASS_ITERATOR], + js_generator_proto_funcs, + countof(js_generator_proto_funcs)); + if (JS_IsException(ctx->class_proto[JS_CLASS_GENERATOR])) + return -1; + ft.generic_magic = js_function_constructor; + obj1 = JS_NewCConstructor(ctx, JS_CLASS_GENERATOR_FUNCTION, "GeneratorFunction", + ft.generic, 1, JS_CFUNC_constructor_or_func_magic, JS_FUNC_GENERATOR, + ctx->function_ctor, + NULL, 0, + js_generator_function_proto_funcs, + countof(js_generator_function_proto_funcs), + JS_NEW_CTOR_NO_GLOBAL | JS_NEW_CTOR_READONLY); + if (JS_IsException(obj1)) + return -1; + JS_FreeValue(ctx, obj1); + if (JS_SetConstructor2(ctx, ctx->class_proto[JS_CLASS_GENERATOR_FUNCTION], + ctx->class_proto[JS_CLASS_GENERATOR], + JS_PROP_CONFIGURABLE, JS_PROP_CONFIGURABLE)) + return -1; + /* global properties */ - ctx->eval_obj = JS_NewCFunction(ctx, js_global_eval, "eval", 1); - JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_eval, - JS_DupValue(ctx, ctx->eval_obj), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - - JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_globalThis, - JS_DupValue(ctx, ctx->global_obj), - JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE); + ctx->eval_obj = JS_GetProperty(ctx, ctx->global_obj, JS_ATOM_eval); + if (JS_IsException(ctx->eval_obj)) + return -1; + + if (JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_globalThis, + JS_DupValue(ctx, ctx->global_obj), + JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE) < 0) + return -1; /* BigInt */ - JS_AddIntrinsicBigInt(ctx); + if (JS_AddIntrinsicBigInt(ctx)) + return -1; + return 0; } /* Typed Arrays */ static uint8_t const typed_array_size_log2[JS_TYPED_ARRAY_COUNT] = { 0, 0, 0, 1, 1, 2, 2, - 3, 3, /* BigInt64Array, BigUint64Array */ - 2, 3 + 3, 3, // BigInt64Array, BigUint64Array + 1, 2, 3 // Float16Array, Float32Array, Float64Array }; static JSValue js_array_buffer_constructor3(JSContext *ctx, JSValueConst new_target, - uint64_t len, JSClassID class_id, + uint64_t len, uint64_t *max_len, + JSClassID class_id, uint8_t *buf, JSFreeArrayBufferDataFunc *free_func, void *opaque, BOOL alloc_flag) @@ -51880,7 +55111,15 @@ static JSValue js_array_buffer_constructor3(JSContext *ctx, JSRuntime *rt = ctx->rt; JSValue obj; JSArrayBuffer *abuf = NULL; + uint64_t sab_alloc_len; + if (!alloc_flag && buf && max_len && free_func != js_array_buffer_free) { + // not observable from JS land, only through C API misuse; + // JS code cannot create externally managed buffers directly + return JS_ThrowInternalError(ctx, + "resizable ArrayBuffers not supported " + "for externally managed buffers"); + } obj = js_create_from_ctor(ctx, new_target, class_id); if (JS_IsException(obj)) return obj; @@ -51889,18 +55128,26 @@ static JSValue js_array_buffer_constructor3(JSContext *ctx, JS_ThrowRangeError(ctx, "invalid array buffer length"); goto fail; } + if (max_len && *max_len > INT32_MAX) { + JS_ThrowRangeError(ctx, "invalid max array buffer length"); + goto fail; + } abuf = js_malloc(ctx, sizeof(*abuf)); if (!abuf) goto fail; abuf->byte_length = len; + abuf->max_byte_length = max_len ? *max_len : -1; if (alloc_flag) { if (class_id == JS_CLASS_SHARED_ARRAY_BUFFER && rt->sab_funcs.sab_alloc) { + // TOOD(bnoordhuis) resizing backing memory for SABs atomically + // is hard so we cheat and allocate |maxByteLength| bytes upfront + sab_alloc_len = max_len ? *max_len : len; abuf->data = rt->sab_funcs.sab_alloc(rt->sab_funcs.sab_opaque, - max_int(len, 1)); + max_int(sab_alloc_len, 1)); if (!abuf->data) goto fail; - memset(abuf->data, 0, len); + memset(abuf->data, 0, sab_alloc_len); } else { /* the allocation must be done after the object creation */ abuf->data = js_mallocz(ctx, max_int(len, 1)); @@ -51936,18 +55183,19 @@ static void js_array_buffer_free(JSRuntime *rt, void *opaque, void *ptr) static JSValue js_array_buffer_constructor2(JSContext *ctx, JSValueConst new_target, - uint64_t len, JSClassID class_id) + uint64_t len, uint64_t *max_len, + JSClassID class_id) { - return js_array_buffer_constructor3(ctx, new_target, len, class_id, + return js_array_buffer_constructor3(ctx, new_target, len, max_len, class_id, NULL, js_array_buffer_free, NULL, TRUE); } static JSValue js_array_buffer_constructor1(JSContext *ctx, JSValueConst new_target, - uint64_t len) + uint64_t len, uint64_t *max_len) { - return js_array_buffer_constructor2(ctx, new_target, len, + return js_array_buffer_constructor2(ctx, new_target, len, max_len, JS_CLASS_ARRAY_BUFFER); } @@ -51955,39 +55203,70 @@ JSValue JS_NewArrayBuffer(JSContext *ctx, uint8_t *buf, size_t len, JSFreeArrayBufferDataFunc *free_func, void *opaque, BOOL is_shared) { - return js_array_buffer_constructor3(ctx, JS_UNDEFINED, len, - is_shared ? JS_CLASS_SHARED_ARRAY_BUFFER : JS_CLASS_ARRAY_BUFFER, + JSClassID class_id = + is_shared ? JS_CLASS_SHARED_ARRAY_BUFFER : JS_CLASS_ARRAY_BUFFER; + return js_array_buffer_constructor3(ctx, JS_UNDEFINED, len, NULL, class_id, buf, free_func, opaque, FALSE); } /* create a new ArrayBuffer of length 'len' and copy 'buf' to it */ JSValue JS_NewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len) { - return js_array_buffer_constructor3(ctx, JS_UNDEFINED, len, + return js_array_buffer_constructor3(ctx, JS_UNDEFINED, len, NULL, JS_CLASS_ARRAY_BUFFER, (uint8_t *)buf, js_array_buffer_free, NULL, TRUE); } +static JSValue js_array_buffer_constructor0(JSContext *ctx, JSValueConst new_target, + int argc, JSValueConst *argv, + JSClassID class_id) + { + uint64_t len, max_len, *pmax_len = NULL; + JSValue obj, val; + int64_t i; + + if (JS_ToIndex(ctx, &len, argv[0])) + return JS_EXCEPTION; + if (argc < 2) + goto next; + if (!JS_IsObject(argv[1])) + goto next; + obj = JS_ToObject(ctx, argv[1]); + if (JS_IsException(obj)) + return JS_EXCEPTION; + val = JS_GetProperty(ctx, obj, JS_ATOM_maxByteLength); + JS_FreeValue(ctx, obj); + if (JS_IsException(val)) + return JS_EXCEPTION; + if (JS_IsUndefined(val)) + goto next; + if (JS_ToInt64Free(ctx, &i, val)) + return JS_EXCEPTION; + // don't have to check i < 0 because len >= 0 + if (len > i || i > MAX_SAFE_INTEGER) + return JS_ThrowRangeError(ctx, "invalid array buffer max length"); + max_len = i; + pmax_len = &max_len; +next: + return js_array_buffer_constructor2(ctx, new_target, len, pmax_len, + class_id); +} + static JSValue js_array_buffer_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) { - uint64_t len; - if (JS_ToIndex(ctx, &len, argv[0])) - return JS_EXCEPTION; - return js_array_buffer_constructor1(ctx, new_target, len); + return js_array_buffer_constructor0(ctx, new_target, argc, argv, + JS_CLASS_ARRAY_BUFFER); } static JSValue js_shared_array_buffer_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) { - uint64_t len; - if (JS_ToIndex(ctx, &len, argv[0])) - return JS_EXCEPTION; - return js_array_buffer_constructor2(ctx, new_target, len, + return js_array_buffer_constructor0(ctx, new_target, argc, argv, JS_CLASS_SHARED_ARRAY_BUFFER); } @@ -52053,6 +55332,23 @@ static JSValue JS_ThrowTypeErrorDetachedArrayBuffer(JSContext *ctx) return JS_ThrowTypeError(ctx, "ArrayBuffer is detached"); } +static JSValue JS_ThrowTypeErrorArrayBufferOOB(JSContext *ctx) +{ + return JS_ThrowTypeError(ctx, "ArrayBuffer is detached or resized"); +} + +// #sec-get-arraybuffer.prototype.detached +static JSValue js_array_buffer_get_detached(JSContext *ctx, + JSValueConst this_val) +{ + JSArrayBuffer *abuf = JS_GetOpaque2(ctx, this_val, JS_CLASS_ARRAY_BUFFER); + if (!abuf) + return JS_EXCEPTION; + if (abuf->shared) + return JS_ThrowTypeError(ctx, "detached called on SharedArrayBuffer"); + return JS_NewBool(ctx, abuf->detached); +} + static JSValue js_array_buffer_get_byteLength(JSContext *ctx, JSValueConst this_val, int class_id) @@ -52064,10 +55360,74 @@ static JSValue js_array_buffer_get_byteLength(JSContext *ctx, return JS_NewUint32(ctx, abuf->byte_length); } +static JSValue js_array_buffer_get_maxByteLength(JSContext *ctx, + JSValueConst this_val, + int class_id) +{ + JSArrayBuffer *abuf = JS_GetOpaque2(ctx, this_val, class_id); + if (!abuf) + return JS_EXCEPTION; + if (array_buffer_is_resizable(abuf)) + return JS_NewUint32(ctx, abuf->max_byte_length); + return JS_NewUint32(ctx, abuf->byte_length); +} + +static JSValue js_array_buffer_get_resizable(JSContext *ctx, + JSValueConst this_val, + int class_id) +{ + JSArrayBuffer *abuf = JS_GetOpaque2(ctx, this_val, class_id); + if (!abuf) + return JS_EXCEPTION; + return JS_NewBool(ctx, array_buffer_is_resizable(abuf)); +} + +static void js_array_buffer_update_typed_arrays(JSArrayBuffer *abuf) +{ + uint32_t size_log2, size_elem; + struct list_head *el; + JSTypedArray *ta; + JSObject *p; + uint8_t *data; + int64_t len; + + len = abuf->byte_length; + data = abuf->data; + // update lengths of all typed arrays backed by this array buffer + list_for_each(el, &abuf->array_list) { + ta = list_entry(el, JSTypedArray, link); + p = ta->obj; + if (p->class_id == JS_CLASS_DATAVIEW) { + if (ta->track_rab) { + if (ta->offset < len) + ta->length = len - ta->offset; + else + ta->length = 0; + } + } else { + p->u.array.count = 0; + p->u.array.u.ptr = NULL; + size_log2 = typed_array_size_log2(p->class_id); + size_elem = 1 << size_log2; + if (ta->track_rab) { + if (len >= (int64_t)ta->offset + size_elem) { + p->u.array.count = (len - ta->offset) >> size_log2; + p->u.array.u.ptr = &data[ta->offset]; + } + } else { + if (len >= (int64_t)ta->offset + ta->length) { + p->u.array.count = ta->length >> size_log2; + p->u.array.u.ptr = &data[ta->offset]; + } + } + } + } + +} + void JS_DetachArrayBuffer(JSContext *ctx, JSValueConst obj) { JSArrayBuffer *abuf = JS_GetOpaque(obj, JS_CLASS_ARRAY_BUFFER); - struct list_head *el; if (!abuf || abuf->detached) return; @@ -52076,19 +55436,7 @@ void JS_DetachArrayBuffer(JSContext *ctx, JSValueConst obj) abuf->data = NULL; abuf->byte_length = 0; abuf->detached = TRUE; - - list_for_each(el, &abuf->array_list) { - JSTypedArray *ta; - JSObject *p; - - ta = list_entry(el, JSTypedArray, link); - p = ta->obj; - /* Note: the typed array length and offset fields are not modified */ - if (p->class_id != JS_CLASS_DATAVIEW) { - p->u.array.count = 0; - p->u.array.u.ptr = NULL; - } - } + js_array_buffer_update_typed_arrays(abuf); } /* get an ArrayBuffer or SharedArrayBuffer */ @@ -52125,6 +55473,142 @@ uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize, JSValueConst obj) return NULL; } +static BOOL array_buffer_is_resizable(const JSArrayBuffer *abuf) +{ + return abuf->max_byte_length >= 0; +} + +// ES #sec-arraybuffer.prototype.transfer +static JSValue js_array_buffer_transfer(JSContext *ctx, + JSValueConst this_val, + int argc, JSValueConst *argv, + int transfer_to_fixed_length) +{ + JSArrayBuffer *abuf; + uint64_t new_len, *pmax_len, max_len; + + abuf = JS_GetOpaque2(ctx, this_val, JS_CLASS_ARRAY_BUFFER); + if (!abuf) + return JS_EXCEPTION; + if (abuf->shared) + return JS_ThrowTypeError(ctx, "cannot transfer a SharedArrayBuffer"); + if (argc < 1 || JS_IsUndefined(argv[0])) + new_len = abuf->byte_length; + else if (JS_ToIndex(ctx, &new_len, argv[0])) + return JS_EXCEPTION; + if (abuf->detached) + return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + pmax_len = NULL; + if (!transfer_to_fixed_length) { + if (array_buffer_is_resizable(abuf)) { // carry over maxByteLength + max_len = abuf->max_byte_length; + if (new_len > max_len) + return JS_ThrowTypeError(ctx, "invalid array buffer length"); + // TODO(bnoordhuis) support externally managed RABs + if (abuf->free_func == js_array_buffer_free) + pmax_len = &max_len; + } + } + /* create an empty AB */ + if (new_len == 0) { + JS_DetachArrayBuffer(ctx, this_val); + return js_array_buffer_constructor2(ctx, JS_UNDEFINED, 0, pmax_len, JS_CLASS_ARRAY_BUFFER); + } else { + uint64_t old_len; + uint8_t *bs, *new_bs; + JSFreeArrayBufferDataFunc *free_func; + + bs = abuf->data; + old_len = abuf->byte_length; + free_func = abuf->free_func; + + /* if length mismatch, realloc. Otherwise, use the same backing buffer. */ + if (new_len != old_len) { + /* XXX: we are currently limited to 2 GB */ + if (new_len > INT32_MAX) + return JS_ThrowRangeError(ctx, "invalid array buffer length"); + + if (free_func != js_array_buffer_free) { + /* cannot use js_realloc() because the buffer was + allocated with a custom allocator */ + new_bs = js_mallocz(ctx, new_len); + if (!new_bs) + return JS_EXCEPTION; + memcpy(new_bs, bs, min_int(old_len, new_len)); + abuf->free_func(ctx->rt, abuf->opaque, bs); + bs = new_bs; + free_func = js_array_buffer_free; + } else { + new_bs = js_realloc(ctx, bs, new_len); + if (!new_bs) + return JS_EXCEPTION; + bs = new_bs; + if (new_len > old_len) + memset(bs + old_len, 0, new_len - old_len); + } + } + /* neuter the backing buffer */ + abuf->data = NULL; + abuf->byte_length = 0; + abuf->detached = TRUE; + js_array_buffer_update_typed_arrays(abuf); + return js_array_buffer_constructor3(ctx, JS_UNDEFINED, new_len, pmax_len, + JS_CLASS_ARRAY_BUFFER, + bs, free_func, + NULL, FALSE); + } +} + +static JSValue js_array_buffer_resize(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int class_id) +{ + JSArrayBuffer *abuf; + uint8_t *data; + int64_t len; + + abuf = JS_GetOpaque2(ctx, this_val, class_id); + if (!abuf) + return JS_EXCEPTION; + if (JS_ToInt64(ctx, &len, argv[0])) + return JS_EXCEPTION; + if (abuf->detached) + return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + if (!array_buffer_is_resizable(abuf)) + return JS_ThrowTypeError(ctx, "array buffer is not resizable"); + // TODO(bnoordhuis) support externally managed RABs + if (abuf->free_func != js_array_buffer_free) + return JS_ThrowTypeError(ctx, "external array buffer is not resizable"); + if (len < 0 || len > abuf->max_byte_length) { + bad_length: + return JS_ThrowRangeError(ctx, "invalid array buffer length"); + } + // SABs can only grow and we don't need to realloc because + // js_array_buffer_constructor3 commits all memory upfront; + // regular RABs are resizable both ways and realloc + if (abuf->shared) { + if (len < abuf->byte_length) + goto bad_length; + // Note this is off-spec; there's supposed to be a single atomic + // |byteLength| property that's shared across SABs but we store + // it per SAB instead. That means when thread A calls sab.grow(2) + // at time t0, and thread B calls sab.grow(1) at time t1, we don't + // throw a TypeError in thread B as the spec says we should, + // instead both threads get their own view of the backing memory, + // 2 bytes big in A, and 1 byte big in B + abuf->byte_length = len; + } else { + data = js_realloc(ctx, abuf->data, max_int(len, 1)); + if (!data) + return JS_EXCEPTION; + if (len > abuf->byte_length) + memset(&data[abuf->byte_length], 0, len - abuf->byte_length); + abuf->byte_length = len; + abuf->data = data; + } + js_array_buffer_update_typed_arrays(abuf); + return JS_UNDEFINED; +} + static JSValue js_array_buffer_slice(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int class_id) @@ -52154,7 +55638,7 @@ static JSValue js_array_buffer_slice(JSContext *ctx, return ctor; if (JS_IsUndefined(ctor)) { new_obj = js_array_buffer_constructor2(ctx, JS_UNDEFINED, new_len, - class_id); + NULL, class_id); } else { JSValue args[1]; args[0] = JS_NewInt64(ctx, new_len); @@ -52180,7 +55664,7 @@ static JSValue js_array_buffer_slice(JSContext *ctx, goto fail; } /* must test again because of side effects */ - if (abuf->detached) { + if (abuf->detached || abuf->byte_length < start + new_len) { JS_ThrowTypeErrorDetachedArrayBuffer(ctx); goto fail; } @@ -52193,7 +55677,13 @@ static JSValue js_array_buffer_slice(JSContext *ctx, static const JSCFunctionListEntry js_array_buffer_proto_funcs[] = { JS_CGETSET_MAGIC_DEF("byteLength", js_array_buffer_get_byteLength, NULL, JS_CLASS_ARRAY_BUFFER ), + JS_CGETSET_MAGIC_DEF("maxByteLength", js_array_buffer_get_maxByteLength, NULL, JS_CLASS_ARRAY_BUFFER ), + JS_CGETSET_MAGIC_DEF("resizable", js_array_buffer_get_resizable, NULL, JS_CLASS_ARRAY_BUFFER ), + JS_CGETSET_DEF("detached", js_array_buffer_get_detached, NULL ), + JS_CFUNC_MAGIC_DEF("resize", 1, js_array_buffer_resize, JS_CLASS_ARRAY_BUFFER ), JS_CFUNC_MAGIC_DEF("slice", 2, js_array_buffer_slice, JS_CLASS_ARRAY_BUFFER ), + JS_CFUNC_MAGIC_DEF("transfer", 0, js_array_buffer_transfer, 0 ), + JS_CFUNC_MAGIC_DEF("transferToFixedLength", 0, js_array_buffer_transfer, 1 ), JS_PROP_STRING_DEF("[Symbol.toStringTag]", "ArrayBuffer", JS_PROP_CONFIGURABLE ), }; @@ -52205,59 +55695,85 @@ static const JSCFunctionListEntry js_shared_array_buffer_funcs[] = { static const JSCFunctionListEntry js_shared_array_buffer_proto_funcs[] = { JS_CGETSET_MAGIC_DEF("byteLength", js_array_buffer_get_byteLength, NULL, JS_CLASS_SHARED_ARRAY_BUFFER ), + JS_CGETSET_MAGIC_DEF("maxByteLength", js_array_buffer_get_maxByteLength, NULL, JS_CLASS_SHARED_ARRAY_BUFFER ), + JS_CGETSET_MAGIC_DEF("growable", js_array_buffer_get_resizable, NULL, JS_CLASS_SHARED_ARRAY_BUFFER ), + JS_CFUNC_MAGIC_DEF("grow", 1, js_array_buffer_resize, JS_CLASS_SHARED_ARRAY_BUFFER ), JS_CFUNC_MAGIC_DEF("slice", 2, js_array_buffer_slice, JS_CLASS_SHARED_ARRAY_BUFFER ), JS_PROP_STRING_DEF("[Symbol.toStringTag]", "SharedArrayBuffer", JS_PROP_CONFIGURABLE ), }; -static JSObject *get_typed_array(JSContext *ctx, - JSValueConst this_val, - int is_dataview) +static JSObject *get_typed_array(JSContext *ctx, JSValueConst this_val) { JSObject *p; if (JS_VALUE_GET_TAG(this_val) != JS_TAG_OBJECT) goto fail; p = JS_VALUE_GET_OBJ(this_val); - if (is_dataview) { - if (p->class_id != JS_CLASS_DATAVIEW) - goto fail; - } else { - if (!(p->class_id >= JS_CLASS_UINT8C_ARRAY && - p->class_id <= JS_CLASS_FLOAT64_ARRAY)) { - fail: - JS_ThrowTypeError(ctx, "not a %s", is_dataview ? "DataView" : "TypedArray"); - return NULL; - } + if (!(p->class_id >= JS_CLASS_UINT8C_ARRAY && + p->class_id <= JS_CLASS_FLOAT64_ARRAY)) { + fail: + JS_ThrowTypeError(ctx, "not a TypedArray"); + return NULL; } return p; } -/* WARNING: 'p' must be a typed array */ -static BOOL typed_array_is_detached(JSContext *ctx, JSObject *p) +// is the typed array detached or out of bounds relative to its RAB? +// |p| must be a typed array, *not* a DataView +static BOOL typed_array_is_oob(JSObject *p) { - JSTypedArray *ta = p->u.typed_array; - JSArrayBuffer *abuf = ta->buffer->u.array_buffer; - /* XXX: could simplify test by ensuring that - p->u.array.u.ptr is NULL iff it is detached */ - return abuf->detached; + JSArrayBuffer *abuf; + JSTypedArray *ta; + int len, size_elem; + int64_t end; + + assert(p->class_id >= JS_CLASS_UINT8C_ARRAY); + assert(p->class_id <= JS_CLASS_FLOAT64_ARRAY); + + ta = p->u.typed_array; + abuf = ta->buffer->u.array_buffer; + if (abuf->detached) + return TRUE; + len = abuf->byte_length; + if (ta->offset > len) + return TRUE; + if (ta->track_rab) + return FALSE; + if (len < (int64_t)ta->offset + ta->length) + return TRUE; + size_elem = 1 << typed_array_size_log2(p->class_id); + end = (int64_t)ta->offset + (int64_t)p->u.array.count * size_elem; + return end > len; } -/* WARNING: 'p' must be a typed array. Works even if the array buffer - is detached */ -static uint32_t typed_array_get_length(JSContext *ctx, JSObject *p) +// Be *very* careful if you touch the typed array's memory directly: +// the length is only valid until the next call into JS land because +// JS code can detach or resize the backing array buffer. Functions +// like JS_GetProperty and JS_ToIndex call JS code. +// +// Exclusively reading or writing elements with JS_GetProperty, +// JS_GetPropertyInt64, JS_SetProperty, etc. is safe because they +// perform bounds checks, as does js_get_fast_array_element. +static int js_typed_array_get_length_unsafe(JSContext *ctx, JSValueConst obj) { - JSTypedArray *ta = p->u.typed_array; - int size_log2 = typed_array_size_log2(p->class_id); - return ta->length >> size_log2; + JSObject *p; + p = get_typed_array(ctx, obj); + if (!p) + return -1; + if (typed_array_is_oob(p)) { + JS_ThrowTypeErrorArrayBufferOOB(ctx); + return -1; + } + return p->u.array.count; } static int validate_typed_array(JSContext *ctx, JSValueConst this_val) { JSObject *p; - p = get_typed_array(ctx, this_val, 0); + p = get_typed_array(ctx, this_val); if (!p) return -1; - if (typed_array_is_detached(ctx, p)) { - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + if (typed_array_is_oob(p)) { + JS_ThrowTypeErrorArrayBufferOOB(ctx); return -1; } return 0; @@ -52267,18 +55783,18 @@ static JSValue js_typed_array_get_length(JSContext *ctx, JSValueConst this_val) { JSObject *p; - p = get_typed_array(ctx, this_val, 0); + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; return JS_NewInt32(ctx, p->u.array.count); } static JSValue js_typed_array_get_buffer(JSContext *ctx, - JSValueConst this_val, int is_dataview) + JSValueConst this_val) { JSObject *p; JSTypedArray *ta; - p = get_typed_array(ctx, this_val, is_dataview); + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; ta = p->u.typed_array; @@ -52286,43 +55802,36 @@ static JSValue js_typed_array_get_buffer(JSContext *ctx, } static JSValue js_typed_array_get_byteLength(JSContext *ctx, - JSValueConst this_val, - int is_dataview) + JSValueConst this_val) { JSObject *p; JSTypedArray *ta; - p = get_typed_array(ctx, this_val, is_dataview); + int size_log2; + + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; - if (typed_array_is_detached(ctx, p)) { - if (is_dataview) { - return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); - } else { - return JS_NewInt32(ctx, 0); - } - } + if (typed_array_is_oob(p)) + return JS_NewInt32(ctx, 0); ta = p->u.typed_array; - return JS_NewInt32(ctx, ta->length); + if (!ta->track_rab) + return JS_NewUint32(ctx, ta->length); + size_log2 = typed_array_size_log2(p->class_id); + return JS_NewInt64(ctx, (int64_t)p->u.array.count << size_log2); } static JSValue js_typed_array_get_byteOffset(JSContext *ctx, - JSValueConst this_val, - int is_dataview) + JSValueConst this_val) { JSObject *p; JSTypedArray *ta; - p = get_typed_array(ctx, this_val, is_dataview); + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; - if (typed_array_is_detached(ctx, p)) { - if (is_dataview) { - return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); - } else { - return JS_NewInt32(ctx, 0); - } - } + if (typed_array_is_oob(p)) + return JS_NewInt32(ctx, 0); ta = p->u.typed_array; - return JS_NewInt32(ctx, ta->offset); + return JS_NewUint32(ctx, ta->offset); } JSValue JS_NewTypedArray(JSContext *ctx, int argc, JSValueConst *argv, @@ -52345,11 +55854,11 @@ JSValue JS_GetTypedArrayBuffer(JSContext *ctx, JSValueConst obj, { JSObject *p; JSTypedArray *ta; - p = get_typed_array(ctx, obj, FALSE); + p = get_typed_array(ctx, obj); if (!p) return JS_EXCEPTION; - if (typed_array_is_detached(ctx, p)) - return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); ta = p->u.typed_array; if (pbyte_offset) *pbyte_offset = ta->offset; @@ -52382,21 +55891,22 @@ static JSValue js_typed_array_set_internal(JSContext *ctx, JSObject *p; JSObject *src_p; uint32_t i; - int64_t src_len, offset; + int64_t dst_len, src_len, offset; JSValue val, src_obj = JS_UNDEFINED; - p = get_typed_array(ctx, dst, 0); + p = get_typed_array(ctx, dst); if (!p) goto fail; if (JS_ToInt64Sat(ctx, &offset, off)) goto fail; if (offset < 0) goto range_error; - if (typed_array_is_detached(ctx, p)) { + if (typed_array_is_oob(p)) { detached: - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + JS_ThrowTypeErrorArrayBufferOOB(ctx); goto fail; } + dst_len = p->u.array.count; src_obj = JS_ToObject(ctx, src); if (JS_IsException(src_obj)) goto fail; @@ -52409,11 +55919,11 @@ static JSValue js_typed_array_set_internal(JSContext *ctx, JSArrayBuffer *src_abuf = src_ta->buffer->u.array_buffer; int shift = typed_array_size_log2(p->class_id); - if (src_abuf->detached) + if (typed_array_is_oob(src_p)) goto detached; src_len = src_p->u.array.count; - if (offset > (int64_t)(p->u.array.count - src_len)) + if (offset > dst_len - src_len) goto range_error; /* copying between typed objects */ @@ -52429,9 +55939,11 @@ static JSValue js_typed_array_set_internal(JSContext *ctx, } /* otherwise, default behavior is slow but correct */ } else { + // can change |dst| as a side effect; per spec, + // perform the range check against its old length if (js_get_length64(ctx, &src_len, src_obj)) goto fail; - if (offset > (int64_t)(p->u.array.count - src_len)) { + if (offset > dst_len - src_len) { range_error: JS_ThrowRangeError(ctx, "invalid array length"); goto fail; @@ -52458,21 +55970,22 @@ static JSValue js_typed_array_at(JSContext *ctx, JSValueConst this_val, JSObject *p; int64_t idx, len; - p = get_typed_array(ctx, this_val, 0); + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; - if (typed_array_is_detached(ctx, p)) { - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); - return JS_EXCEPTION; - } + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + len = p->u.array.count; + // note: can change p->u.array.count if (JS_ToInt64Sat(ctx, &idx, argv[0])) return JS_EXCEPTION; - len = p->u.array.count; if (idx < 0) idx = len + idx; + + len = p->u.array.count; if (idx < 0 || idx >= len) return JS_UNDEFINED; return JS_GetPropertyInt64(ctx, this_val, idx); @@ -52485,16 +55998,16 @@ static JSValue js_typed_array_with(JSContext *ctx, JSValueConst this_val, JSObject *p; int64_t idx, len; - p = get_typed_array(ctx, this_val, /*is_dataview*/0); + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; - if (typed_array_is_detached(ctx, p)) + if (typed_array_is_oob(p)) return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + len = p->u.array.count; if (JS_ToInt64Sat(ctx, &idx, argv[0])) return JS_EXCEPTION; - len = p->u.array.count; if (idx < 0) idx = len + idx; @@ -52502,11 +56015,11 @@ static JSValue js_typed_array_with(JSContext *ctx, JSValueConst this_val, if (JS_IsException(val)) return JS_EXCEPTION; - if (typed_array_is_detached(ctx, p) || idx < 0 || idx >= len) + if (typed_array_is_oob(p) || idx < 0 || idx >= p->u.array.count) return JS_ThrowRangeError(ctx, "invalid array index"); arr = js_typed_array_constructor_ta(ctx, JS_UNDEFINED, this_val, - p->class_id); + p->class_id, len); if (JS_IsException(arr)) { JS_FreeValue(ctx, val); return JS_EXCEPTION; @@ -52537,41 +56050,6 @@ static JSValue js_create_typed_array_iterator(JSContext *ctx, JSValueConst this_ return js_create_array_iterator(ctx, this_val, argc, argv, magic); } -/* return < 0 if exception */ -static int js_typed_array_get_length_internal(JSContext *ctx, - JSValueConst obj) -{ - JSObject *p; - p = get_typed_array(ctx, obj, 0); - if (!p) - return -1; - if (typed_array_is_detached(ctx, p)) { - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); - return -1; - } - return p->u.array.count; -} - -#if 0 -/* validate a typed array and return its length */ -static JSValue js_typed_array___getLength(JSContext *ctx, - JSValueConst this_val, - int argc, JSValueConst *argv) -{ - BOOL ignore_detached = JS_ToBool(ctx, argv[1]); - - if (ignore_detached) { - return js_typed_array_get_length(ctx, argv[0]); - } else { - int len; - len = js_typed_array_get_length_internal(ctx, argv[0]); - if (len < 0) - return JS_EXCEPTION; - return JS_NewInt32(ctx, len); - } -} -#endif - static JSValue js_typed_array_create(JSContext *ctx, JSValueConst ctor, int argc, JSValueConst *argv) { @@ -52583,7 +56061,7 @@ static JSValue js_typed_array_create(JSContext *ctx, JSValueConst ctor, if (JS_IsException(ret)) return ret; /* validate the typed array */ - new_len = js_typed_array_get_length_internal(ctx, ret); + new_len = js_typed_array_get_length_unsafe(ctx, ret); if (new_len < 0) goto fail; if (argc == 1) { @@ -52619,7 +56097,7 @@ static JSValue js_typed_array___speciesCreate(JSContext *ctx, int argc1; obj = argv[0]; - p = get_typed_array(ctx, obj, 0); + p = get_typed_array(ctx, obj); if (!p) return JS_EXCEPTION; ctor = JS_SpeciesConstructor(ctx, obj, JS_UNDEFINED); @@ -52740,11 +56218,14 @@ static JSValue js_typed_array_copyWithin(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { JSObject *p; - int len, to, from, final, count, shift; + int len, to, from, final, count, shift, space; - len = js_typed_array_get_length_internal(ctx, this_val); - if (len < 0) + p = get_typed_array(ctx, this_val); + if (!p) return JS_EXCEPTION; + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + len = p->u.array.count; if (JS_ToInt32Clamp(ctx, &to, argv[0], 0, len, len)) return JS_EXCEPTION; @@ -52758,11 +56239,14 @@ static JSValue js_typed_array_copyWithin(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; } + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + + // RAB may have been resized by evil .valueOf method + space = p->u.array.count - max_int(to, from); count = min_int(final - from, len - to); + count = min_int(count, space); if (count > 0) { - p = JS_VALUE_GET_OBJ(this_val); - if (typed_array_is_detached(ctx, p)) - return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); shift = typed_array_size_log2(p->class_id); memmove(p->u.array.u.uint8_ptr + (to << shift), p->u.array.u.uint8_ptr + (from << shift), @@ -52778,10 +56262,12 @@ static JSValue js_typed_array_fill(JSContext *ctx, JSValueConst this_val, int len, k, final, shift; uint64_t v64; - len = js_typed_array_get_length_internal(ctx, this_val); - if (len < 0) + p = get_typed_array(ctx, this_val); + if (!p) return JS_EXCEPTION; - p = JS_VALUE_GET_OBJ(this_val); + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + len = p->u.array.count; if (p->class_id == JS_CLASS_UINT8C_ARRAY) { int32_t v; @@ -52800,7 +56286,9 @@ static JSValue js_typed_array_fill(JSContext *ctx, JSValueConst this_val, double d; if (JS_ToFloat64(ctx, &d, argv[0])) return JS_EXCEPTION; - if (p->class_id == JS_CLASS_FLOAT32_ARRAY) { + if (p->class_id == JS_CLASS_FLOAT16_ARRAY) { + v64 = tofp16(d); + } else if (p->class_id == JS_CLASS_FLOAT32_ARRAY) { union { float f; uint32_t u32; @@ -52826,9 +56314,11 @@ static JSValue js_typed_array_fill(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; } - if (typed_array_is_detached(ctx, p)) - return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + // RAB may have been resized by evil .valueOf method + final = min_int(final, p->u.array.count); shift = typed_array_size_log2(p->class_id); switch(shift) { case 0: @@ -52867,7 +56357,7 @@ static JSValue js_typed_array_find(JSContext *ctx, JSValueConst this_val, int dir; val = JS_UNDEFINED; - len = js_typed_array_get_length_internal(ctx, this_val); + len = js_typed_array_get_length_unsafe(ctx, this_val); if (len < 0) goto exception; @@ -52931,32 +56421,27 @@ static JSValue js_typed_array_indexOf(JSContext *ctx, JSValueConst this_val, int64_t v64; double d; float f; + uint16_t hf; + + p = get_typed_array(ctx, this_val); + if (!p) + return JS_EXCEPTION; + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + len = p->u.array.count; - len = js_typed_array_get_length_internal(ctx, this_val); - if (len < 0) - goto exception; if (len == 0) goto done; if (special == special_lastIndexOf) { k = len - 1; if (argc > 1) { - if (JS_ToFloat64(ctx, &d, argv[1])) + int64_t k1; + if (JS_ToInt64Clamp(ctx, &k1, argv[1], -1, len - 1, len)) goto exception; - if (isnan(d)) { - k = 0; - } else { - if (d >= 0) { - if (d < k) { - k = d; - } - } else { - d += len; - if (d < 0) - goto done; - k = d; - } - } + k = k1; + if (k < 0) + goto done; } stop = -1; inc = -1; @@ -52970,16 +56455,23 @@ static JSValue js_typed_array_indexOf(JSContext *ctx, JSValueConst this_val, inc = 1; } - p = JS_VALUE_GET_OBJ(this_val); - /* if the array was detached, no need to go further (but no - exception is raised) */ - if (typed_array_is_detached(ctx, p)) { - /* "includes" scans all the properties, so "undefined" can match */ - if (special == special_includes && JS_IsUndefined(argv[0]) && len > 0) - res = 0; + /* includes function: 'undefined' can be found if searching out of bounds */ + if (len > p->u.array.count && special == special_includes && + JS_IsUndefined(argv[0]) && k < len) { + res = 0; goto done; } + // RAB may have been resized by evil .valueOf method + len = min_int(len, p->u.array.count); + if (len == 0) + goto done; + if (special == special_lastIndexOf) + k = min_int(k, len - 1); + else + k = min_int(k, len); + stop = min_int(stop, len); + is_bigint = 0; is_int = 0; /* avoid warning */ v64 = 0; /* avoid warning */ @@ -53042,7 +56534,9 @@ static JSValue js_typed_array_indexOf(JSContext *ctx, JSValueConst this_val, pv = p->u.array.u.uint8_ptr; v = v64; if (inc > 0) { - pp = memchr(pv + k, v, len - k); + pp = NULL; + if (pv) + pp = memchr(pv + k, v, len - k); if (pp) res = pp - pv; } else { @@ -53093,6 +56587,39 @@ static JSValue js_typed_array_indexOf(JSContext *ctx, JSValueConst this_val, } } break; + case JS_CLASS_FLOAT16_ARRAY: + if (is_bigint) + break; + if (isnan(d)) { + const uint16_t *pv = p->u.array.u.fp16_ptr; + /* special case: indexOf returns -1, includes finds NaN */ + if (special != special_includes) + goto done; + for (; k != stop; k += inc) { + if (isfp16nan(pv[k])) { + res = k; + break; + } + } + } else if (d == 0) { + // special case: includes also finds negative zero + const uint16_t *pv = p->u.array.u.fp16_ptr; + for (; k != stop; k += inc) { + if (isfp16zero(pv[k])) { + res = k; + break; + } + } + } else if (hf = tofp16(d), d == fromfp16(hf)) { + const uint16_t *pv = p->u.array.u.fp16_ptr; + for (; k != stop; k += inc) { + if (pv[k] == hf) { + res = k; + break; + } + } + } + break; case JS_CLASS_FLOAT32_ARRAY: if (is_bigint) break; @@ -53178,35 +56705,42 @@ static JSValue js_typed_array_join(JSContext *ctx, JSValueConst this_val, { JSValue sep = JS_UNDEFINED, el; StringBuffer b_s, *b = &b_s; - JSString *p = NULL; - int i, n; + JSString *s = NULL; + JSObject *p; + int i, len, oldlen, newlen; int c; - n = js_typed_array_get_length_internal(ctx, this_val); - if (n < 0) - goto exception; + p = get_typed_array(ctx, this_val); + if (!p) + return JS_EXCEPTION; + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + len = oldlen = newlen = p->u.array.count; c = ','; /* default separator */ if (!toLocaleString && argc > 0 && !JS_IsUndefined(argv[0])) { sep = JS_ToString(ctx, argv[0]); if (JS_IsException(sep)) goto exception; - p = JS_VALUE_GET_STRING(sep); - if (p->len == 1 && !p->is_wide_char) - c = p->u.str8[0]; + s = JS_VALUE_GET_STRING(sep); + if (s->len == 1 && !s->is_wide_char) + c = s->u.str8[0]; else c = -1; + // ToString(sep) can detach or resize the arraybuffer as a side effect + newlen = p->u.array.count; + len = min_int(len, newlen); } string_buffer_init(ctx, b, 0); /* XXX: optimize with direct access */ - for(i = 0; i < n; i++) { + for(i = 0; i < len; i++) { if (i > 0) { if (c >= 0) { if (string_buffer_putc8(b, c)) goto fail; } else { - if (string_buffer_concat(b, p, 0, p->len)) + if (string_buffer_concat(b, s, 0, s->len)) goto fail; } } @@ -53222,6 +56756,19 @@ static JSValue js_typed_array_join(JSContext *ctx, JSValueConst this_val, goto fail; } } + + // add extra separators in case RAB was resized by evil .valueOf method + i = max_int(1, newlen); + for(/*empty*/; i < oldlen; i++) { + if (c >= 0) { + if (string_buffer_putc8(b, c)) + goto fail; + } else { + if (string_buffer_concat(b, s, 0, s->len)) + goto fail; + } + } + JS_FreeValue(ctx, sep); return string_buffer_end(b); @@ -53238,7 +56785,7 @@ static JSValue js_typed_array_reverse(JSContext *ctx, JSValueConst this_val, JSObject *p; int len; - len = js_typed_array_get_length_internal(ctx, this_val); + len = js_typed_array_get_length_unsafe(ctx, this_val); if (len < 0) return JS_EXCEPTION; if (len > 0) { @@ -53301,11 +56848,11 @@ static JSValue js_typed_array_toReversed(JSContext *ctx, JSValueConst this_val, JSValue arr, ret; JSObject *p; - p = get_typed_array(ctx, this_val, /*is_dataview*/0); + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; arr = js_typed_array_constructor_ta(ctx, JS_UNDEFINED, this_val, - p->class_id); + p->class_id, p->u.array.count); if (JS_IsException(arr)) return JS_EXCEPTION; ret = js_typed_array_reverse(ctx, arr, argc, argv); @@ -53331,12 +56878,15 @@ static JSValue js_typed_array_slice(JSContext *ctx, JSValueConst this_val, JSValueConst args[2]; JSValue arr, val; JSObject *p, *p1; - int n, len, start, final, count, shift; + int n, len, start, final, count, shift, space; arr = JS_UNDEFINED; - len = js_typed_array_get_length_internal(ctx, this_val); - if (len < 0) + p = get_typed_array(ctx, this_val); + if (!p) goto exception; + if (typed_array_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + len = p->u.array.count; if (JS_ToInt32Clamp(ctx, &start, argv[0], 0, len, len)) goto exception; @@ -53347,9 +56897,6 @@ static JSValue js_typed_array_slice(JSContext *ctx, JSValueConst this_val, } count = max_int(final - start, 0); - p = get_typed_array(ctx, this_val, 0); - if (p == NULL) - goto exception; shift = typed_array_size_log2(p->class_id); args[0] = this_val; @@ -53363,10 +56910,10 @@ static JSValue js_typed_array_slice(JSContext *ctx, JSValueConst this_val, || validate_typed_array(ctx, arr)) goto exception; - p1 = get_typed_array(ctx, arr, 0); - if (p1 != NULL && p->class_id == p1->class_id && - typed_array_get_length(ctx, p1) >= count && - typed_array_get_length(ctx, p) >= start + count) { + p1 = get_typed_array(ctx, arr); + space = max_int(0, p->u.array.count - start); + count = min_int(count, space); + if (p1 != NULL && p->class_id == p1->class_id) { slice_memcpy(p1->u.array.u.uint8_ptr, p->u.array.u.uint8_ptr + (start << shift), count << shift); @@ -53392,37 +56939,41 @@ static JSValue js_typed_array_subarray(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { JSValueConst args[4]; - JSValue arr, byteOffset, ta_buffer; + JSValue arr, ta_buffer; + JSTypedArray *ta; JSObject *p; int len, start, final, count, shift, offset; - - p = get_typed_array(ctx, this_val, 0); + BOOL is_auto; + + p = get_typed_array(ctx, this_val); if (!p) goto exception; len = p->u.array.count; if (JS_ToInt32Clamp(ctx, &start, argv[0], 0, len, len)) goto exception; + shift = typed_array_size_log2(p->class_id); + ta = p->u.typed_array; + /* Read byteOffset (ta->offset) even if detached */ + offset = ta->offset + (start << shift); + final = len; - if (!JS_IsUndefined(argv[1])) { + if (JS_IsUndefined(argv[1])) { + is_auto = ta->track_rab; + } else { + is_auto = FALSE; if (JS_ToInt32Clamp(ctx, &final, argv[1], 0, len, len)) goto exception; - } + } count = max_int(final - start, 0); - byteOffset = js_typed_array_get_byteOffset(ctx, this_val, 0); - if (JS_IsException(byteOffset)) - goto exception; - shift = typed_array_size_log2(p->class_id); - offset = JS_VALUE_GET_INT(byteOffset) + (start << shift); - JS_FreeValue(ctx, byteOffset); - ta_buffer = js_typed_array_get_buffer(ctx, this_val, 0); + ta_buffer = js_typed_array_get_buffer(ctx, this_val); if (JS_IsException(ta_buffer)) goto exception; args[0] = this_val; args[1] = ta_buffer; args[2] = JS_NewInt32(ctx, offset); args[3] = JS_NewInt32(ctx, count); - arr = js_typed_array___speciesCreate(ctx, JS_UNDEFINED, 4, args); + arr = js_typed_array___speciesCreate(ctx, JS_UNDEFINED, is_auto ? 3 : 4, args); JS_FreeValue(ctx, ta_buffer); return arr; @@ -53483,6 +57034,11 @@ static int js_TA_cmp_uint64(const void *a, const void *b, void *opaque) { return (y < x) - (y > x); } +static int js_TA_cmp_float16(const void *a, const void *b, void *opaque) { + return js_cmp_doubles(fromfp16(*(const uint16_t *)a), + fromfp16(*(const uint16_t *)b)); +} + static int js_TA_cmp_float32(const void *a, const void *b, void *opaque) { return js_cmp_doubles(*(const float *)a, *(const float *)b); } @@ -53523,6 +57079,10 @@ static JSValue js_TA_get_uint64(JSContext *ctx, const void *a) { return JS_NewBigUint64(ctx, *(uint64_t *)a); } +static JSValue js_TA_get_float16(JSContext *ctx, const void *a) { + return __JS_NewFloat64(ctx, fromfp16(*(const uint16_t *)a)); +} + static JSValue js_TA_get_float32(JSContext *ctx, const void *a) { return __JS_NewFloat64(ctx, *(const float *)a); } @@ -53537,7 +57097,6 @@ struct TA_sort_context { JSValueConst arr; JSValueConst cmp; JSValue (*getfun)(JSContext *ctx, const void *a); - uint8_t *array_ptr; /* cannot change unless the array is detached */ int elt_size; }; @@ -53548,16 +57107,23 @@ static int js_TA_cmp_generic(const void *a, const void *b, void *opaque) { JSValueConst argv[2]; JSValue res; int cmp; - + JSObject *p; + cmp = 0; if (!psc->exception) { /* Note: the typed array can be detached without causing an error */ a_idx = *(uint32_t *)a; b_idx = *(uint32_t *)b; - argv[0] = psc->getfun(ctx, psc->array_ptr + + p = JS_VALUE_GET_PTR(psc->arr); + if (a_idx >= p->u.array.count || b_idx >= p->u.array.count) { + /* OOB case */ + psc->exception = 2; + return 0; + } + argv[0] = psc->getfun(ctx, p->u.array.u.uint8_ptr + a_idx * (size_t)psc->elt_size); - argv[1] = psc->getfun(ctx, psc->array_ptr + + argv[1] = psc->getfun(ctx, p->u.array.u.uint8_ptr + b_idx * (size_t)(psc->elt_size)); res = JS_Call(ctx, psc->cmp, JS_UNDEFINED, 2, argv); if (JS_IsException(res)) { @@ -53580,10 +57146,6 @@ static int js_TA_cmp_generic(const void *a, const void *b, void *opaque) { /* make sort stable: compare array offsets */ cmp = (a_idx > b_idx) - (a_idx < b_idx); } - if (unlikely(typed_array_is_detached(ctx, - JS_VALUE_GET_PTR(psc->arr)))) { - psc->exception = 2; - } done: JS_FreeValue(ctx, (JSValue)argv[0]); JS_FreeValue(ctx, (JSValue)argv[1]); @@ -53598,7 +57160,6 @@ static JSValue js_typed_array_sort(JSContext *ctx, JSValueConst this_val, int len; size_t elt_size; struct TA_sort_context tsc; - void *array_ptr; int (*cmpfun)(const void *a, const void *b, void *opaque); tsc.ctx = ctx; @@ -53608,7 +57169,7 @@ static JSValue js_typed_array_sort(JSContext *ctx, JSValueConst this_val, if (!JS_IsUndefined(tsc.cmp) && check_function(ctx, tsc.cmp)) return JS_EXCEPTION; - len = js_typed_array_get_length_internal(ctx, this_val); + len = js_typed_array_get_length_unsafe(ctx, this_val); if (len < 0) return JS_EXCEPTION; @@ -53648,6 +57209,10 @@ static JSValue js_typed_array_sort(JSContext *ctx, JSValueConst this_val, tsc.getfun = js_TA_get_uint64; cmpfun = js_TA_cmp_uint64; break; + case JS_CLASS_FLOAT16_ARRAY: + tsc.getfun = js_TA_get_float16; + cmpfun = js_TA_cmp_float16; + break; case JS_CLASS_FLOAT32_ARRAY: tsc.getfun = js_TA_get_float32; cmpfun = js_TA_cmp_float32; @@ -53659,7 +57224,6 @@ static JSValue js_typed_array_sort(JSContext *ctx, JSValueConst this_val, default: abort(); } - array_ptr = p->u.array.u.ptr; elt_size = 1 << typed_array_size_log2(p->class_id); if (!JS_IsUndefined(tsc.cmp)) { uint32_t *array_idx; @@ -53672,7 +57236,6 @@ static JSValue js_typed_array_sort(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; for(i = 0; i < len; i++) array_idx[i] = i; - tsc.array_ptr = array_ptr; tsc.elt_size = elt_size; rqsort(array_idx, len, sizeof(array_idx[0]), js_TA_cmp_generic, &tsc); @@ -53681,46 +57244,50 @@ static JSValue js_typed_array_sort(JSContext *ctx, JSValueConst this_val, goto fail; /* detached typed array during the sort: no error */ } else { - array_tmp = js_malloc(ctx, len * elt_size); - if (!array_tmp) { - fail: - js_free(ctx, array_idx); - return JS_EXCEPTION; - } - memcpy(array_tmp, array_ptr, len * elt_size); - switch(elt_size) { - case 1: - for(i = 0; i < len; i++) { - j = array_idx[i]; - ((uint8_t *)array_ptr)[i] = ((uint8_t *)array_tmp)[j]; - } - break; - case 2: - for(i = 0; i < len; i++) { - j = array_idx[i]; - ((uint16_t *)array_ptr)[i] = ((uint16_t *)array_tmp)[j]; - } - break; - case 4: - for(i = 0; i < len; i++) { - j = array_idx[i]; - ((uint32_t *)array_ptr)[i] = ((uint32_t *)array_tmp)[j]; + void *array_ptr = p->u.array.u.ptr; + len = min_int(len, p->u.array.count); + if (len != 0) { + array_tmp = js_malloc(ctx, len * elt_size); + if (!array_tmp) { + fail: + js_free(ctx, array_idx); + return JS_EXCEPTION; } - break; - case 8: - for(i = 0; i < len; i++) { - j = array_idx[i]; - ((uint64_t *)array_ptr)[i] = ((uint64_t *)array_tmp)[j]; + memcpy(array_tmp, array_ptr, len * elt_size); + switch(elt_size) { + case 1: + for(i = 0; i < len; i++) { + j = array_idx[i]; + ((uint8_t *)array_ptr)[i] = ((uint8_t *)array_tmp)[j]; + } + break; + case 2: + for(i = 0; i < len; i++) { + j = array_idx[i]; + ((uint16_t *)array_ptr)[i] = ((uint16_t *)array_tmp)[j]; + } + break; + case 4: + for(i = 0; i < len; i++) { + j = array_idx[i]; + ((uint32_t *)array_ptr)[i] = ((uint32_t *)array_tmp)[j]; + } + break; + case 8: + for(i = 0; i < len; i++) { + j = array_idx[i]; + ((uint64_t *)array_ptr)[i] = ((uint64_t *)array_tmp)[j]; + } + break; + default: + abort(); } - break; - default: - abort(); + js_free(ctx, array_tmp); } - js_free(ctx, array_tmp); } js_free(ctx, array_idx); } else { - rqsort(array_ptr, len, elt_size, cmpfun, &tsc); + rqsort(p->u.array.u.ptr, len, elt_size, cmpfun, &tsc); if (tsc.exception) return JS_EXCEPTION; } @@ -53734,11 +57301,11 @@ static JSValue js_typed_array_toSorted(JSContext *ctx, JSValueConst this_val, JSValue arr, ret; JSObject *p; - p = get_typed_array(ctx, this_val, /*is_dataview*/0); + p = get_typed_array(ctx, this_val); if (!p) return JS_EXCEPTION; arr = js_typed_array_constructor_ta(ctx, JS_UNDEFINED, this_val, - p->class_id); + p->class_id, p->u.array.count); if (JS_IsException(arr)) return JS_EXCEPTION; ret = js_typed_array_sort(ctx, arr, argc, argv); @@ -53750,18 +57317,15 @@ static const JSCFunctionListEntry js_typed_array_base_funcs[] = { JS_CFUNC_DEF("from", 1, js_typed_array_from ), JS_CFUNC_DEF("of", 0, js_typed_array_of ), JS_CGETSET_DEF("[Symbol.species]", js_get_this, NULL ), - //JS_CFUNC_DEF("__getLength", 2, js_typed_array___getLength ), - //JS_CFUNC_DEF("__create", 2, js_typed_array___create ), - //JS_CFUNC_DEF("__speciesCreate", 2, js_typed_array___speciesCreate ), }; static const JSCFunctionListEntry js_typed_array_base_proto_funcs[] = { JS_CGETSET_DEF("length", js_typed_array_get_length, NULL ), JS_CFUNC_DEF("at", 1, js_typed_array_at ), JS_CFUNC_DEF("with", 2, js_typed_array_with ), - JS_CGETSET_MAGIC_DEF("buffer", js_typed_array_get_buffer, NULL, 0 ), - JS_CGETSET_MAGIC_DEF("byteLength", js_typed_array_get_byteLength, NULL, 0 ), - JS_CGETSET_MAGIC_DEF("byteOffset", js_typed_array_get_byteOffset, NULL, 0 ), + JS_CGETSET_DEF("buffer", js_typed_array_get_buffer, NULL ), + JS_CGETSET_DEF("byteLength", js_typed_array_get_byteLength, NULL ), + JS_CGETSET_DEF("byteOffset", js_typed_array_get_byteOffset, NULL ), JS_CFUNC_DEF("set", 1, js_typed_array_set ), JS_CFUNC_MAGIC_DEF("values", 0, js_create_typed_array_iterator, JS_ITERATOR_KIND_VALUE ), JS_ALIAS_DEF("[Symbol.iterator]", "values" ), @@ -53795,6 +57359,13 @@ static const JSCFunctionListEntry js_typed_array_base_proto_funcs[] = { //JS_ALIAS_BASE_DEF("toString", "toString", 2 /* Array.prototype. */), @@@ }; +static const JSCFunctionListEntry js_typed_array_funcs[] = { + JS_PROP_INT32_DEF("BYTES_PER_ELEMENT", 1, 0), + JS_PROP_INT32_DEF("BYTES_PER_ELEMENT", 2, 0), + JS_PROP_INT32_DEF("BYTES_PER_ELEMENT", 4, 0), + JS_PROP_INT32_DEF("BYTES_PER_ELEMENT", 8, 0), +}; + static JSValue js_typed_array_base_constructor(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) @@ -53804,7 +57375,8 @@ static JSValue js_typed_array_base_constructor(JSContext *ctx, /* 'obj' must be an allocated typed array object */ static int typed_array_init(JSContext *ctx, JSValueConst obj, - JSValue buffer, uint64_t offset, uint64_t len) + JSValue buffer, uint64_t offset, uint64_t len, + BOOL track_rab) { JSTypedArray *ta; JSObject *p, *pbuffer; @@ -53824,6 +57396,7 @@ static int typed_array_init(JSContext *ctx, JSValueConst obj, ta->buffer = pbuffer; ta->offset = offset; ta->length = len << size_log2; + ta->track_rab = track_rab; list_add_tail(&ta->link, &abuf->array_list); p->u.typed_array = ta; p->u.array.count = len; @@ -53903,10 +57476,11 @@ static JSValue js_typed_array_constructor_obj(JSContext *ctx, } buffer = js_array_buffer_constructor1(ctx, JS_UNDEFINED, - len << size_log2); + len << size_log2, + NULL); if (JS_IsException(buffer)) goto fail; - if (typed_array_init(ctx, ret, buffer, 0, len)) + if (typed_array_init(ctx, ret, buffer, 0, len, /*track_rab*/FALSE)) goto fail; for(i = 0; i < len; i++) { @@ -53927,12 +57501,12 @@ static JSValue js_typed_array_constructor_obj(JSContext *ctx, static JSValue js_typed_array_constructor_ta(JSContext *ctx, JSValueConst new_target, JSValueConst src_obj, - int classid) + int classid, uint32_t len) { JSObject *p, *src_buffer; JSTypedArray *ta; JSValue obj, buffer; - uint32_t len, i; + uint32_t i; int size_log2; JSArrayBuffer *src_abuf, *abuf; @@ -53940,27 +57514,27 @@ static JSValue js_typed_array_constructor_ta(JSContext *ctx, if (JS_IsException(obj)) return obj; p = JS_VALUE_GET_OBJ(src_obj); - if (typed_array_is_detached(ctx, p)) { - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + if (typed_array_is_oob(p)) { + JS_ThrowTypeErrorArrayBufferOOB(ctx); goto fail; } ta = p->u.typed_array; - len = p->u.array.count; src_buffer = ta->buffer; src_abuf = src_buffer->u.array_buffer; size_log2 = typed_array_size_log2(classid); buffer = js_array_buffer_constructor1(ctx, JS_UNDEFINED, - (uint64_t)len << size_log2); + (uint64_t)len << size_log2, + NULL); if (JS_IsException(buffer)) goto fail; /* necessary because it could have been detached */ - if (typed_array_is_detached(ctx, p)) { + if (typed_array_is_oob(p)) { JS_FreeValue(ctx, buffer); - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + JS_ThrowTypeErrorArrayBufferOOB(ctx); goto fail; } abuf = JS_GetOpaque(buffer, JS_CLASS_ARRAY_BUFFER); - if (typed_array_init(ctx, obj, buffer, 0, len)) + if (typed_array_init(ctx, obj, buffer, 0, len, /*track_rab*/FALSE)) goto fail; if (p->class_id == classid) { /* same type: copy the content */ @@ -53986,6 +57560,7 @@ static JSValue js_typed_array_constructor(JSContext *ctx, int argc, JSValueConst *argv, int classid) { + BOOL track_rab = FALSE; JSValue buffer, obj; JSArrayBuffer *abuf; int size_log2; @@ -53996,7 +57571,8 @@ static JSValue js_typed_array_constructor(JSContext *ctx, if (JS_ToIndex(ctx, &len, argv[0])) return JS_EXCEPTION; buffer = js_array_buffer_constructor1(ctx, JS_UNDEFINED, - len << size_log2); + len << size_log2, + NULL); if (JS_IsException(buffer)) return JS_EXCEPTION; offset = 0; @@ -54013,8 +57589,10 @@ static JSValue js_typed_array_constructor(JSContext *ctx, offset > abuf->byte_length) return JS_ThrowRangeError(ctx, "invalid offset"); if (JS_IsUndefined(argv[2])) { - if ((abuf->byte_length & ((1 << size_log2) - 1)) != 0) - goto invalid_length; + track_rab = array_buffer_is_resizable(abuf); + if (!track_rab) + if ((abuf->byte_length & ((1 << size_log2) - 1)) != 0) + goto invalid_length; len = (abuf->byte_length - offset) >> size_log2; } else { if (JS_ToIndex(ctx, &len, argv[2])) @@ -54030,7 +57608,8 @@ static JSValue js_typed_array_constructor(JSContext *ctx, } else { if (p->class_id >= JS_CLASS_UINT8C_ARRAY && p->class_id <= JS_CLASS_FLOAT64_ARRAY) { - return js_typed_array_constructor_ta(ctx, new_target, argv[0], classid); + return js_typed_array_constructor_ta(ctx, new_target, argv[0], + classid, p->u.array.count); } else { return js_typed_array_constructor_obj(ctx, new_target, argv[0], classid); } @@ -54042,7 +57621,7 @@ static JSValue js_typed_array_constructor(JSContext *ctx, JS_FreeValue(ctx, buffer); return JS_EXCEPTION; } - if (typed_array_init(ctx, obj, buffer, offset, len)) { + if (typed_array_init(ctx, obj, buffer, offset, len, track_rab)) { JS_FreeValue(ctx, obj); return JS_EXCEPTION; } @@ -54078,6 +57657,8 @@ static JSValue js_dataview_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) { + BOOL recompute_len = FALSE; + BOOL track_rab = FALSE; JSArrayBuffer *abuf; uint64_t offset; uint32_t len; @@ -54107,6 +57688,9 @@ static JSValue js_dataview_constructor(JSContext *ctx, if (l > len) return JS_ThrowRangeError(ctx, "invalid byteLength"); len = l; + } else { + recompute_len = TRUE; + track_rab = array_buffer_is_resizable(abuf); } obj = js_create_from_ctor(ctx, new_target, JS_CLASS_DATAVIEW); @@ -54117,6 +57701,16 @@ static JSValue js_dataview_constructor(JSContext *ctx, JS_ThrowTypeErrorDetachedArrayBuffer(ctx); goto fail; } + // RAB could have been resized in js_create_from_ctor() + if (offset > abuf->byte_length) { + goto out_of_bound; + } else if (recompute_len) { + len = abuf->byte_length - offset; + } else if (offset + len > abuf->byte_length) { + out_of_bound: + JS_ThrowRangeError(ctx, "invalid byteOffset or byteLength"); + goto fail; + } ta = js_malloc(ctx, sizeof(*ta)); if (!ta) { fail: @@ -54128,11 +57722,88 @@ static JSValue js_dataview_constructor(JSContext *ctx, ta->buffer = JS_VALUE_GET_OBJ(JS_DupValue(ctx, buffer)); ta->offset = offset; ta->length = len; + ta->track_rab = track_rab; list_add_tail(&ta->link, &abuf->array_list); p->u.typed_array = ta; return obj; } +// is the DataView out of bounds relative to its parent arraybuffer? +static BOOL dataview_is_oob(JSObject *p) +{ + JSArrayBuffer *abuf; + JSTypedArray *ta; + + assert(p->class_id == JS_CLASS_DATAVIEW); + ta = p->u.typed_array; + abuf = ta->buffer->u.array_buffer; + if (abuf->detached) + return TRUE; + if (ta->offset > abuf->byte_length) + return TRUE; + if (ta->track_rab) + return FALSE; + return (int64_t)ta->offset + ta->length > abuf->byte_length; +} + +static JSObject *get_dataview(JSContext *ctx, JSValueConst this_val) +{ + JSObject *p; + if (JS_VALUE_GET_TAG(this_val) != JS_TAG_OBJECT) + goto fail; + p = JS_VALUE_GET_OBJ(this_val); + if (p->class_id != JS_CLASS_DATAVIEW) { + fail: + JS_ThrowTypeError(ctx, "not a DataView"); + return NULL; + } + return p; +} + +static JSValue js_dataview_get_buffer(JSContext *ctx, JSValueConst this_val) +{ + JSObject *p; + JSTypedArray *ta; + p = get_dataview(ctx, this_val); + if (!p) + return JS_EXCEPTION; + ta = p->u.typed_array; + return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, ta->buffer)); +} + +static JSValue js_dataview_get_byteLength(JSContext *ctx, JSValueConst this_val) +{ + JSArrayBuffer *abuf; + JSTypedArray *ta; + JSObject *p; + + p = get_dataview(ctx, this_val); + if (!p) + return JS_EXCEPTION; + if (dataview_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + ta = p->u.typed_array; + if (ta->track_rab) { + abuf = ta->buffer->u.array_buffer; + return JS_NewUint32(ctx, abuf->byte_length - ta->offset); + } + return JS_NewUint32(ctx, ta->length); +} + +static JSValue js_dataview_get_byteOffset(JSContext *ctx, JSValueConst this_val) +{ + JSTypedArray *ta; + JSObject *p; + + p = get_dataview(ctx, this_val); + if (!p) + return JS_EXCEPTION; + if (dataview_is_oob(p)) + return JS_ThrowTypeErrorArrayBufferOOB(ctx); + ta = p->u.typed_array; + return JS_NewUint32(ctx, ta->offset); +} + static JSValue js_dataview_getValue(JSContext *ctx, JSValueConst this_obj, int argc, JSValueConst *argv, int class_id) @@ -54156,8 +57827,14 @@ static JSValue js_dataview_getValue(JSContext *ctx, abuf = ta->buffer->u.array_buffer; if (abuf->detached) return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + // order matters: this check should come before the next one if ((pos + size) > ta->length) return JS_ThrowRangeError(ctx, "out of bound"); + // test262 expects a TypeError for this and V8, in its infinite wisdom, + // throws a "detached array buffer" exception, but IMO that doesn't make + // sense because the buffer is not in fact detached, it's still there + if ((int64_t)ta->offset + ta->length > abuf->byte_length) + return JS_ThrowTypeError(ctx, "out of bound"); ptr = abuf->data + ta->offset + pos; switch(class_id) { @@ -54203,6 +57880,14 @@ static JSValue js_dataview_getValue(JSContext *ctx, return JS_NewBigUint64(ctx, v); } break; + case JS_CLASS_FLOAT16_ARRAY: + { + uint16_t v; + v = get_u16(ptr); + if (is_swap) + v = bswap16(v); + return __JS_NewFloat64(ctx, fromfp16(v)); + } case JS_CLASS_FLOAT32_ARRAY: { union { @@ -54264,7 +57949,9 @@ static JSValue js_dataview_setValue(JSContext *ctx, double d; if (JS_ToFloat64(ctx, &d, val)) return JS_EXCEPTION; - if (class_id == JS_CLASS_FLOAT32_ARRAY) { + if (class_id == JS_CLASS_FLOAT16_ARRAY) { + v = tofp16(d); + } else if (class_id == JS_CLASS_FLOAT32_ARRAY) { union { float f; uint32_t i; @@ -54282,8 +57969,14 @@ static JSValue js_dataview_setValue(JSContext *ctx, abuf = ta->buffer->u.array_buffer; if (abuf->detached) return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + // order matters: this check should come before the next one if ((pos + size) > ta->length) return JS_ThrowRangeError(ctx, "out of bound"); + // test262 expects a TypeError for this and V8, in its infinite wisdom, + // throws a "detached array buffer" exception, but IMO that doesn't make + // sense because the buffer is not in fact detached, it's still there + if ((int64_t)ta->offset + ta->length > abuf->byte_length) + return JS_ThrowTypeError(ctx, "out of bound"); ptr = abuf->data + ta->offset + pos; switch(class_id) { @@ -54293,6 +57986,7 @@ static JSValue js_dataview_setValue(JSContext *ctx, break; case JS_CLASS_INT16_ARRAY: case JS_CLASS_UINT16_ARRAY: + case JS_CLASS_FLOAT16_ARRAY: if (is_swap) v = bswap16(v); put_u16(ptr, v); @@ -54318,9 +58012,9 @@ static JSValue js_dataview_setValue(JSContext *ctx, } static const JSCFunctionListEntry js_dataview_proto_funcs[] = { - JS_CGETSET_MAGIC_DEF("buffer", js_typed_array_get_buffer, NULL, 1 ), - JS_CGETSET_MAGIC_DEF("byteLength", js_typed_array_get_byteLength, NULL, 1 ), - JS_CGETSET_MAGIC_DEF("byteOffset", js_typed_array_get_byteOffset, NULL, 1 ), + JS_CGETSET_DEF("buffer", js_dataview_get_buffer, NULL ), + JS_CGETSET_DEF("byteLength", js_dataview_get_byteLength, NULL ), + JS_CGETSET_DEF("byteOffset", js_dataview_get_byteOffset, NULL ), JS_CFUNC_MAGIC_DEF("getInt8", 1, js_dataview_getValue, JS_CLASS_INT8_ARRAY ), JS_CFUNC_MAGIC_DEF("getUint8", 1, js_dataview_getValue, JS_CLASS_UINT8_ARRAY ), JS_CFUNC_MAGIC_DEF("getInt16", 1, js_dataview_getValue, JS_CLASS_INT16_ARRAY ), @@ -54329,6 +58023,7 @@ static const JSCFunctionListEntry js_dataview_proto_funcs[] = { JS_CFUNC_MAGIC_DEF("getUint32", 1, js_dataview_getValue, JS_CLASS_UINT32_ARRAY ), JS_CFUNC_MAGIC_DEF("getBigInt64", 1, js_dataview_getValue, JS_CLASS_BIG_INT64_ARRAY ), JS_CFUNC_MAGIC_DEF("getBigUint64", 1, js_dataview_getValue, JS_CLASS_BIG_UINT64_ARRAY ), + JS_CFUNC_MAGIC_DEF("getFloat16", 1, js_dataview_getValue, JS_CLASS_FLOAT16_ARRAY ), JS_CFUNC_MAGIC_DEF("getFloat32", 1, js_dataview_getValue, JS_CLASS_FLOAT32_ARRAY ), JS_CFUNC_MAGIC_DEF("getFloat64", 1, js_dataview_getValue, JS_CLASS_FLOAT64_ARRAY ), JS_CFUNC_MAGIC_DEF("setInt8", 2, js_dataview_setValue, JS_CLASS_INT8_ARRAY ), @@ -54339,6 +58034,7 @@ static const JSCFunctionListEntry js_dataview_proto_funcs[] = { JS_CFUNC_MAGIC_DEF("setUint32", 2, js_dataview_setValue, JS_CLASS_UINT32_ARRAY ), JS_CFUNC_MAGIC_DEF("setBigInt64", 2, js_dataview_setValue, JS_CLASS_BIG_INT64_ARRAY ), JS_CFUNC_MAGIC_DEF("setBigUint64", 2, js_dataview_setValue, JS_CLASS_BIG_UINT64_ARRAY ), + JS_CFUNC_MAGIC_DEF("setFloat16", 2, js_dataview_setValue, JS_CLASS_FLOAT16_ARRAY ), JS_CFUNC_MAGIC_DEF("setFloat32", 2, js_dataview_setValue, JS_CLASS_FLOAT32_ARRAY ), JS_CFUNC_MAGIC_DEF("setFloat64", 2, js_dataview_setValue, JS_CLASS_FLOAT64_ARRAY ), JS_PROP_STRING_DEF("[Symbol.toStringTag]", "DataView", JS_PROP_CONFIGURABLE ), @@ -54358,11 +58054,12 @@ typedef enum AtomicsOpEnum { ATOMICS_OP_LOAD, } AtomicsOpEnum; -static void *js_atomics_get_ptr(JSContext *ctx, - JSArrayBuffer **pabuf, - int *psize_log2, JSClassID *pclass_id, - JSValueConst obj, JSValueConst idx_val, - int is_waitable) +static int js_atomics_get_ptr(JSContext *ctx, + void **pptr, + JSArrayBuffer **pabuf, + int *psize_log2, JSClassID *pclass_id, + JSValueConst obj, JSValueConst idx_val, + int is_waitable) { JSObject *p; JSTypedArray *ta; @@ -54370,7 +58067,7 @@ static void *js_atomics_get_ptr(JSContext *ctx, void *ptr; uint64_t idx; BOOL err; - int size_log2; + int size_log2, old_len; if (JS_VALUE_GET_TAG(obj) != JS_TAG_OBJECT) goto fail; @@ -54384,33 +58081,46 @@ static void *js_atomics_get_ptr(JSContext *ctx, if (err) { fail: JS_ThrowTypeError(ctx, "integer TypedArray expected"); - return NULL; + return -1; } ta = p->u.typed_array; abuf = ta->buffer->u.array_buffer; if (!abuf->shared) { if (is_waitable == 2) { JS_ThrowTypeError(ctx, "not a SharedArrayBuffer TypedArray"); - return NULL; + return -1; } if (abuf->detached) { JS_ThrowTypeErrorDetachedArrayBuffer(ctx); - return NULL; + return -1; } } + old_len = p->u.array.count; + if (JS_ToIndex(ctx, &idx, idx_val)) { - return NULL; - } - /* RevalidateAtomicAccess(): must test again detached after JS_ToIndex() */ - if (abuf->detached) { - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); - return NULL; + return -1; } - /* if the array buffer is detached, p->u.array.count = 0 */ - if (idx >= p->u.array.count) { - JS_ThrowRangeError(ctx, "out-of-bound access"); - return NULL; + + if (idx >= old_len) + goto oob; + + if (is_waitable == 1) { + /* notify(): just avoid having an invalid pointer if overflow */ + if (idx >= p->u.array.count) + ptr = NULL; + } else { + /* RevalidateAtomicAccess() */ + if (typed_array_is_oob(p)) { + JS_ThrowTypeErrorArrayBufferOOB(ctx); + return -1; + } + if (idx >= p->u.array.count) { + oob: + JS_ThrowRangeError(ctx, "out-of-bound access"); + return -1; + } } + size_log2 = typed_array_size_log2(p->class_id); ptr = p->u.array.u.uint8_ptr + ((uintptr_t)idx << size_log2); if (pabuf) @@ -54419,7 +58129,8 @@ static void *js_atomics_get_ptr(JSContext *ctx, *psize_log2 = size_log2; if (pclass_id) *pclass_id = p->class_id; - return ptr; + *pptr = ptr; + return 0; } static JSValue js_atomics_op(JSContext *ctx, @@ -54433,9 +58144,8 @@ static JSValue js_atomics_op(JSContext *ctx, JSClassID class_id; JSArrayBuffer *abuf; - ptr = js_atomics_get_ptr(ctx, &abuf, &size_log2, &class_id, - argv[0], argv[1], 0); - if (!ptr) + if (js_atomics_get_ptr(ctx, &ptr, &abuf, &size_log2, &class_id, + argv[0], argv[1], 0)) return JS_EXCEPTION; rep_val = 0; if (op == ATOMICS_OP_LOAD) { @@ -54576,9 +58286,8 @@ static JSValue js_atomics_store(JSContext *ctx, JSValue ret; JSArrayBuffer *abuf; - ptr = js_atomics_get_ptr(ctx, &abuf, &size_log2, NULL, - argv[0], argv[1], 0); - if (!ptr) + if (js_atomics_get_ptr(ctx, &ptr, &abuf, &size_log2, NULL, + argv[0], argv[1], 0)) return JS_EXCEPTION; if (size_log2 == 3) { int64_t v64; @@ -54643,6 +58352,49 @@ static pthread_mutex_t js_atomics_mutex = PTHREAD_MUTEX_INITIALIZER; static struct list_head js_atomics_waiter_list = LIST_HEAD_INIT(js_atomics_waiter_list); +#if defined(__aarch64__) +static inline void cpu_pause(void) +{ + asm volatile("yield" ::: "memory"); +} +#elif defined(__x86_64) || defined(__i386__) +static inline void cpu_pause(void) +{ + asm volatile("pause" ::: "memory"); +} +#else +static inline void cpu_pause(void) +{ +} +#endif + +// no-op: Atomics.pause() is not allowed to block or yield to another +// thread, only to hint the CPU that it should back off for a bit; +// the amount of work we do here is a good enough substitute +static JSValue js_atomics_pause(JSContext *ctx, JSValueConst this_obj, + int argc, JSValueConst *argv) +{ + double d; + + if (argc > 0) { + switch (JS_VALUE_GET_NORM_TAG(argv[0])) { + case JS_TAG_FLOAT64: // accepted if and only if fraction == 0.0 + d = JS_VALUE_GET_FLOAT64(argv[0]); + if (isfinite(d)) + if (0 == modf(d, &d)) + break; + // fallthru + default: + return JS_ThrowTypeError(ctx, "not an integral number"); + case JS_TAG_UNDEFINED: + case JS_TAG_INT: + break; + } + } + cpu_pause(); + return JS_UNDEFINED; +} + static JSValue js_atomics_wait(JSContext *ctx, JSValueConst this_obj, int argc, JSValueConst *argv) @@ -54656,9 +58408,8 @@ static JSValue js_atomics_wait(JSContext *ctx, int ret, size_log2, res; double d; - ptr = js_atomics_get_ptr(ctx, NULL, &size_log2, NULL, - argv[0], argv[1], 2); - if (!ptr) + if (js_atomics_get_ptr(ctx, &ptr, NULL, &size_log2, NULL, + argv[0], argv[1], 2)) return JS_EXCEPTION; if (size_log2 == 3) { if (JS_ToBigInt64(ctx, &v, argv[2])) @@ -54736,18 +58487,15 @@ static JSValue js_atomics_notify(JSContext *ctx, JSAtomicsWaiter *waiter; JSArrayBuffer *abuf; - ptr = js_atomics_get_ptr(ctx, &abuf, NULL, NULL, argv[0], argv[1], 1); - if (!ptr) + if (js_atomics_get_ptr(ctx, &ptr, &abuf, NULL, NULL, argv[0], argv[1], 1)) return JS_EXCEPTION; - + if (JS_IsUndefined(argv[2])) { count = INT32_MAX; } else { if (JS_ToInt32Clamp(ctx, &count, argv[2], 0, INT32_MAX, 0)) return JS_EXCEPTION; } - if (abuf->detached) - return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); n = 0; if (abuf->shared && count > 0) { @@ -54784,6 +58532,7 @@ static const JSCFunctionListEntry js_atomics_funcs[] = { JS_CFUNC_MAGIC_DEF("load", 2, js_atomics_op, ATOMICS_OP_LOAD ), JS_CFUNC_DEF("store", 3, js_atomics_store ), JS_CFUNC_DEF("isLockFree", 1, js_atomics_isLockFree ), + JS_CFUNC_DEF("pause", 0, js_atomics_pause ), JS_CFUNC_DEF("wait", 4, js_atomics_wait ), JS_CFUNC_DEF("notify", 3, js_atomics_notify ), JS_PROP_STRING_DEF("[Symbol.toStringTag]", "Atomics", JS_PROP_CONFIGURABLE ), @@ -54793,100 +58542,106 @@ static const JSCFunctionListEntry js_atomics_obj[] = { JS_OBJECT_DEF("Atomics", js_atomics_funcs, countof(js_atomics_funcs), JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE ), }; -void JS_AddIntrinsicAtomics(JSContext *ctx) +static int JS_AddIntrinsicAtomics(JSContext *ctx) { /* add Atomics as autoinit object */ - JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_atomics_obj, countof(js_atomics_obj)); + return JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_atomics_obj, countof(js_atomics_obj)); } #endif /* CONFIG_ATOMICS */ -void JS_AddIntrinsicTypedArrays(JSContext *ctx) +int JS_AddIntrinsicTypedArrays(JSContext *ctx) { - JSValue typed_array_base_proto, typed_array_base_func; - JSValueConst array_buffer_func, shared_array_buffer_func; - int i; + JSValue typed_array_base_func, typed_array_base_proto, obj; + int i, ret; - ctx->class_proto[JS_CLASS_ARRAY_BUFFER] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_ARRAY_BUFFER], - js_array_buffer_proto_funcs, - countof(js_array_buffer_proto_funcs)); - - array_buffer_func = JS_NewGlobalCConstructorOnly(ctx, "ArrayBuffer", - js_array_buffer_constructor, 1, - ctx->class_proto[JS_CLASS_ARRAY_BUFFER]); - JS_SetPropertyFunctionList(ctx, array_buffer_func, - js_array_buffer_funcs, - countof(js_array_buffer_funcs)); - - ctx->class_proto[JS_CLASS_SHARED_ARRAY_BUFFER] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_SHARED_ARRAY_BUFFER], - js_shared_array_buffer_proto_funcs, - countof(js_shared_array_buffer_proto_funcs)); - - shared_array_buffer_func = JS_NewGlobalCConstructorOnly(ctx, "SharedArrayBuffer", - js_shared_array_buffer_constructor, 1, - ctx->class_proto[JS_CLASS_SHARED_ARRAY_BUFFER]); - JS_SetPropertyFunctionList(ctx, shared_array_buffer_func, - js_shared_array_buffer_funcs, - countof(js_shared_array_buffer_funcs)); - - typed_array_base_proto = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, typed_array_base_proto, - js_typed_array_base_proto_funcs, - countof(js_typed_array_base_proto_funcs)); + obj = JS_NewCConstructor(ctx, JS_CLASS_ARRAY_BUFFER, "ArrayBuffer", + js_array_buffer_constructor, 1, JS_CFUNC_constructor, 0, + JS_UNDEFINED, + js_array_buffer_funcs, countof(js_array_buffer_funcs), + js_array_buffer_proto_funcs, countof(js_array_buffer_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; + JS_FreeValue(ctx, obj); + + obj = JS_NewCConstructor(ctx, JS_CLASS_SHARED_ARRAY_BUFFER, "SharedArrayBuffer", + js_shared_array_buffer_constructor, 1, JS_CFUNC_constructor, 0, + JS_UNDEFINED, + js_shared_array_buffer_funcs, countof(js_shared_array_buffer_funcs), + js_shared_array_buffer_proto_funcs, countof(js_shared_array_buffer_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; + JS_FreeValue(ctx, obj); - /* TypedArray.prototype.toString must be the same object as Array.prototype.toString */ - JSValue obj = JS_GetProperty(ctx, ctx->class_proto[JS_CLASS_ARRAY], JS_ATOM_toString); - /* XXX: should use alias method in JSCFunctionListEntry */ //@@@ - JS_DefinePropertyValue(ctx, typed_array_base_proto, JS_ATOM_toString, obj, - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - typed_array_base_func = JS_NewCFunction2(ctx, js_typed_array_base_constructor, - "TypedArray", 0, JS_CFUNC_constructor_or_func, 0); - JS_SetPropertyFunctionList(ctx, typed_array_base_func, - js_typed_array_base_funcs, - countof(js_typed_array_base_funcs)); - JS_SetConstructor(ctx, typed_array_base_func, typed_array_base_proto); + typed_array_base_func = + JS_NewCConstructor(ctx, -1, "TypedArray", + js_typed_array_base_constructor, 0, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + js_typed_array_base_funcs, countof(js_typed_array_base_funcs), + js_typed_array_base_proto_funcs, countof(js_typed_array_base_proto_funcs), + JS_NEW_CTOR_NO_GLOBAL); + if (JS_IsException(typed_array_base_func)) + return -1; + /* TypedArray.prototype.toString must be the same object as Array.prototype.toString */ + obj = JS_GetProperty(ctx, ctx->class_proto[JS_CLASS_ARRAY], JS_ATOM_toString); + if (JS_IsException(obj)) + goto fail; + /* XXX: should use alias method in JSCFunctionListEntry */ //@@@ + typed_array_base_proto = JS_GetProperty(ctx, typed_array_base_func, JS_ATOM_prototype); + if (JS_IsException(typed_array_base_proto)) + goto fail; + ret = JS_DefinePropertyValue(ctx, typed_array_base_proto, JS_ATOM_toString, obj, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_FreeValue(ctx, typed_array_base_proto); + if (ret < 0) + goto fail; + /* Used to squelch a -Wcast-function-type warning. */ JSCFunctionType ft = { .generic_magic = js_typed_array_constructor }; for(i = JS_CLASS_UINT8C_ARRAY; i < JS_CLASS_UINT8C_ARRAY + JS_TYPED_ARRAY_COUNT; i++) { - JSValue func_obj; char buf[ATOM_GET_STR_BUF_SIZE]; const char *name; - - ctx->class_proto[i] = JS_NewObjectProto(ctx, typed_array_base_proto); - JS_DefinePropertyValueStr(ctx, ctx->class_proto[i], - "BYTES_PER_ELEMENT", - JS_NewInt32(ctx, 1 << typed_array_size_log2(i)), - 0); + const JSCFunctionListEntry *bpe; + name = JS_AtomGetStr(ctx, buf, sizeof(buf), JS_ATOM_Uint8ClampedArray + i - JS_CLASS_UINT8C_ARRAY); - func_obj = JS_NewCFunction3(ctx, ft.generic, - name, 3, JS_CFUNC_constructor_magic, i, - typed_array_base_func); - JS_NewGlobalCConstructor2(ctx, func_obj, name, ctx->class_proto[i]); - JS_DefinePropertyValueStr(ctx, func_obj, - "BYTES_PER_ELEMENT", - JS_NewInt32(ctx, 1 << typed_array_size_log2(i)), - 0); + bpe = js_typed_array_funcs + typed_array_size_log2(i); + obj = JS_NewCConstructor(ctx, i, name, + ft.generic, 3, JS_CFUNC_constructor_magic, i, + typed_array_base_func, + bpe, 1, + bpe, 1, + 0); + if (JS_IsException(obj)) { + fail: + JS_FreeValue(ctx, typed_array_base_func); + return -1; + } + JS_FreeValue(ctx, obj); } - JS_FreeValue(ctx, typed_array_base_proto); JS_FreeValue(ctx, typed_array_base_func); /* DataView */ - ctx->class_proto[JS_CLASS_DATAVIEW] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_DATAVIEW], - js_dataview_proto_funcs, - countof(js_dataview_proto_funcs)); - JS_NewGlobalCConstructorOnly(ctx, "DataView", - js_dataview_constructor, 1, - ctx->class_proto[JS_CLASS_DATAVIEW]); + obj = JS_NewCConstructor(ctx, JS_CLASS_DATAVIEW, "DataView", + js_dataview_constructor, 1, JS_CFUNC_constructor, 0, + JS_UNDEFINED, + NULL, 0, + js_dataview_proto_funcs, countof(js_dataview_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; + JS_FreeValue(ctx, obj); + /* Atomics */ #ifdef CONFIG_ATOMICS - JS_AddIntrinsicAtomics(ctx); + if (JS_AddIntrinsicAtomics(ctx)) + return -1; #endif + return 0; } /* WeakRef */ @@ -54972,7 +58727,7 @@ typedef struct JSFinRecEntry { typedef struct JSFinalizationRegistryData { JSWeakRefHeader weakref_header; struct list_head entries; /* list of JSFinRecEntry.link */ - JSContext *ctx; + JSContext *realm; JSValue cb; } JSFinalizationRegistryData; @@ -54989,6 +58744,7 @@ static void js_finrec_finalizer(JSRuntime *rt, JSValue val) js_free_rt(rt, fre); } JS_FreeValueRT(rt, frd->cb); + JS_FreeContext(frd->realm); list_del(&frd->weakref_header.link); js_free_rt(rt, frd); } @@ -55005,6 +58761,7 @@ static void js_finrec_mark(JSRuntime *rt, JSValueConst val, JS_MarkValue(rt, fre->held_val, mark_func); } JS_MarkValue(rt, frd->cb, mark_func); + mark_func(rt, &frd->realm->header); } } @@ -55030,7 +58787,7 @@ static void finrec_delete_weakref(JSRuntime *rt, JSWeakRefHeader *wh) JSValueConst args[2]; args[0] = frd->cb; args[1] = fre->held_val; - JS_EnqueueJob(frd->ctx, js_finrec_job, 2, args); + JS_EnqueueJob(frd->realm, js_finrec_job, 2, args); js_weakref_free(rt, fre->target); js_weakref_free(rt, fre->token); @@ -55065,7 +58822,7 @@ static JSValue js_finrec_constructor(JSContext *ctx, JSValueConst new_target, frd->weakref_header.weakref_type = JS_WEAKREF_TYPE_FINREC; list_add_tail(&frd->weakref_header.link, &ctx->rt->weakref_list); init_list_head(&frd->entries); - frd->ctx = ctx; /* XXX: JS_DupContext() ? */ + frd->realm = JS_DupContext(ctx); frd->cb = JS_DupValue(ctx, cb); JS_SetOpaque(obj, frd); return obj; @@ -55139,29 +58896,42 @@ static const JSClassShortDef js_finrec_class_def[] = { { JS_ATOM_FinalizationRegistry, js_finrec_finalizer, js_finrec_mark }, /* JS_CLASS_FINALIZATION_REGISTRY */ }; -void JS_AddIntrinsicWeakRef(JSContext *ctx) +int JS_AddIntrinsicWeakRef(JSContext *ctx) { JSRuntime *rt = ctx->rt; - + JSValue obj; + /* WeakRef */ if (!JS_IsRegisteredClass(rt, JS_CLASS_WEAK_REF)) { - init_class_range(rt, js_weakref_class_def, JS_CLASS_WEAK_REF, - countof(js_weakref_class_def)); + if (init_class_range(rt, js_weakref_class_def, JS_CLASS_WEAK_REF, + countof(js_weakref_class_def))) + return -1; } - ctx->class_proto[JS_CLASS_WEAK_REF] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_WEAK_REF], - js_weakref_proto_funcs, - countof(js_weakref_proto_funcs)); - JS_NewGlobalCConstructor(ctx, "WeakRef", js_weakref_constructor, 1, ctx->class_proto[JS_CLASS_WEAK_REF]); + obj = JS_NewCConstructor(ctx, JS_CLASS_WEAK_REF, "WeakRef", + js_weakref_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + NULL, 0, + js_weakref_proto_funcs, countof(js_weakref_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; + JS_FreeValue(ctx, obj); /* FinalizationRegistry */ if (!JS_IsRegisteredClass(rt, JS_CLASS_FINALIZATION_REGISTRY)) { - init_class_range(rt, js_finrec_class_def, JS_CLASS_FINALIZATION_REGISTRY, - countof(js_finrec_class_def)); - } - ctx->class_proto[JS_CLASS_FINALIZATION_REGISTRY] = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_FINALIZATION_REGISTRY], - js_finrec_proto_funcs, - countof(js_finrec_proto_funcs)); - JS_NewGlobalCConstructor(ctx, "FinalizationRegistry", js_finrec_constructor, 1, ctx->class_proto[JS_CLASS_FINALIZATION_REGISTRY]); + if (init_class_range(rt, js_finrec_class_def, JS_CLASS_FINALIZATION_REGISTRY, + countof(js_finrec_class_def))) + return -1; + } + + obj = JS_NewCConstructor(ctx, JS_CLASS_FINALIZATION_REGISTRY, "FinalizationRegistry", + js_finrec_constructor, 1, JS_CFUNC_constructor_or_func, 0, + JS_UNDEFINED, + NULL, 0, + js_finrec_proto_funcs, countof(js_finrec_proto_funcs), + 0); + if (JS_IsException(obj)) + return -1; + JS_FreeValue(ctx, obj); + return 0; } diff --git a/src/couch_quickjs/quickjs/quickjs.h b/src/couch_quickjs/quickjs/quickjs.h index b851cd9ca7..92cc000d0a 100644 --- a/src/couch_quickjs/quickjs/quickjs.h +++ b/src/couch_quickjs/quickjs/quickjs.h @@ -319,8 +319,7 @@ static inline JSValue __JS_NewShortBigInt(JSContext *ctx, int64_t d) (JS_SetProperty) */ #define JS_PROP_THROW_STRICT (1 << 15) -#define JS_PROP_NO_ADD (1 << 16) /* internal use */ -#define JS_PROP_NO_EXOTIC (1 << 17) /* internal use */ +#define JS_PROP_NO_EXOTIC (1 << 16) /* internal use */ #ifndef JS_DEFAULT_STACK_SIZE #define JS_DEFAULT_STACK_SIZE (1024 * 1024) @@ -395,18 +394,18 @@ JSValue JS_GetClassProto(JSContext *ctx, JSClassID class_id); /* the following functions are used to select the intrinsic object to save memory */ JSContext *JS_NewContextRaw(JSRuntime *rt); -void JS_AddIntrinsicBaseObjects(JSContext *ctx); -void JS_AddIntrinsicDate(JSContext *ctx); -void JS_AddIntrinsicEval(JSContext *ctx); -void JS_AddIntrinsicStringNormalize(JSContext *ctx); +int JS_AddIntrinsicBaseObjects(JSContext *ctx); +int JS_AddIntrinsicDate(JSContext *ctx); +int JS_AddIntrinsicEval(JSContext *ctx); +int JS_AddIntrinsicStringNormalize(JSContext *ctx); void JS_AddIntrinsicRegExpCompiler(JSContext *ctx); -void JS_AddIntrinsicRegExp(JSContext *ctx); -void JS_AddIntrinsicJSON(JSContext *ctx); -void JS_AddIntrinsicProxy(JSContext *ctx); -void JS_AddIntrinsicMapSet(JSContext *ctx); -void JS_AddIntrinsicTypedArrays(JSContext *ctx); -void JS_AddIntrinsicPromise(JSContext *ctx); -void JS_AddIntrinsicWeakRef(JSContext *ctx); +int JS_AddIntrinsicRegExp(JSContext *ctx); +int JS_AddIntrinsicJSON(JSContext *ctx); +int JS_AddIntrinsicProxy(JSContext *ctx); +int JS_AddIntrinsicMapSet(JSContext *ctx); +int JS_AddIntrinsicTypedArrays(JSContext *ctx); +int JS_AddIntrinsicPromise(JSContext *ctx); +int JS_AddIntrinsicWeakRef(JSContext *ctx); JSValue js_string_codePointRange(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); @@ -456,7 +455,11 @@ void JS_FreeAtom(JSContext *ctx, JSAtom v); void JS_FreeAtomRT(JSRuntime *rt, JSAtom v); JSValue JS_AtomToValue(JSContext *ctx, JSAtom atom); JSValue JS_AtomToString(JSContext *ctx, JSAtom atom); -const char *JS_AtomToCString(JSContext *ctx, JSAtom atom); +const char *JS_AtomToCStringLen(JSContext *ctx, size_t *plen, JSAtom atom); +static inline const char *JS_AtomToCString(JSContext *ctx, JSAtom atom) +{ + return JS_AtomToCStringLen(ctx, NULL, atom); +} JSAtom JS_ValueToAtom(JSContext *ctx, JSValueConst val); /* object class support */ @@ -805,6 +808,8 @@ JSValue JS_GetPrototype(JSContext *ctx, JSValueConst val); int JS_GetOwnPropertyNames(JSContext *ctx, JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj, int flags); +void JS_FreePropertyEnum(JSContext *ctx, JSPropertyEnum *tab, + uint32_t len); int JS_GetOwnProperty(JSContext *ctx, JSPropertyDescriptor *desc, JSValueConst obj, JSAtom prop); @@ -871,6 +876,7 @@ typedef enum JSTypedArrayEnum { JS_TYPED_ARRAY_UINT32, JS_TYPED_ARRAY_BIG_INT64, JS_TYPED_ARRAY_BIG_UINT64, + JS_TYPED_ARRAY_FLOAT16, JS_TYPED_ARRAY_FLOAT32, JS_TYPED_ARRAY_FLOAT64, } JSTypedArrayEnum; @@ -929,12 +935,25 @@ typedef char *JSModuleNormalizeFunc(JSContext *ctx, const char *module_name, void *opaque); typedef JSModuleDef *JSModuleLoaderFunc(JSContext *ctx, const char *module_name, void *opaque); - +typedef JSModuleDef *JSModuleLoaderFunc2(JSContext *ctx, + const char *module_name, void *opaque, + JSValueConst attributes); +/* return -1 if exception, 0 if OK */ +typedef int JSModuleCheckSupportedImportAttributes(JSContext *ctx, void *opaque, + JSValueConst attributes); + /* module_normalize = NULL is allowed and invokes the default module filename normalizer */ void JS_SetModuleLoaderFunc(JSRuntime *rt, JSModuleNormalizeFunc *module_normalize, JSModuleLoaderFunc *module_loader, void *opaque); +/* same as JS_SetModuleLoaderFunc but with attributes. if + module_check_attrs = NULL, no attribute checking is done. */ +void JS_SetModuleLoaderFunc2(JSRuntime *rt, + JSModuleNormalizeFunc *module_normalize, + JSModuleLoaderFunc2 *module_loader, + JSModuleCheckSupportedImportAttributes *module_check_attrs, + void *opaque); /* return the import.meta object of a module */ JSValue JS_GetImportMeta(JSContext *ctx, JSModuleDef *m); JSAtom JS_GetModuleName(JSContext *ctx, JSModuleDef *m); @@ -1029,10 +1048,12 @@ static inline JSValue JS_NewCFunctionMagic(JSContext *ctx, JSCFunctionMagic *fun const char *name, int length, JSCFunctionEnum cproto, int magic) { - return JS_NewCFunction2(ctx, (JSCFunction *)func, name, length, cproto, magic); + /* Used to squelch a -Wcast-function-type warning. */ + JSCFunctionType ft = { .generic_magic = func }; + return JS_NewCFunction2(ctx, ft.generic, name, length, cproto, magic); } -void JS_SetConstructor(JSContext *ctx, JSValueConst func_obj, - JSValueConst proto); +int JS_SetConstructor(JSContext *ctx, JSValueConst func_obj, + JSValueConst proto); /* C property definition */ @@ -1076,6 +1097,8 @@ typedef struct JSCFunctionListEntry { #define JS_DEF_PROP_UNDEFINED 7 #define JS_DEF_OBJECT 8 #define JS_DEF_ALIAS 9 +#define JS_DEF_PROP_ATOM 10 +#define JS_DEF_PROP_BOOL 11 /* Note: c++ does not like nested designators */ #define JS_CFUNC_DEF(name, length, func1) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_CFUNC, 0, .u = { .func = { length, JS_CFUNC_generic, { .generic = func1 } } } } @@ -1089,13 +1112,15 @@ typedef struct JSCFunctionListEntry { #define JS_PROP_INT64_DEF(name, val, prop_flags) { name, prop_flags, JS_DEF_PROP_INT64, 0, .u = { .i64 = val } } #define JS_PROP_DOUBLE_DEF(name, val, prop_flags) { name, prop_flags, JS_DEF_PROP_DOUBLE, 0, .u = { .f64 = val } } #define JS_PROP_UNDEFINED_DEF(name, prop_flags) { name, prop_flags, JS_DEF_PROP_UNDEFINED, 0, .u = { .i32 = 0 } } +#define JS_PROP_ATOM_DEF(name, val, prop_flags) { name, prop_flags, JS_DEF_PROP_ATOM, 0, .u = { .i32 = val } } +#define JS_PROP_BOOL_DEF(name, val, prop_flags) { name, prop_flags, JS_DEF_PROP_BOOL, 0, .u = { .i32 = val } } #define JS_OBJECT_DEF(name, tab, len, prop_flags) { name, prop_flags, JS_DEF_OBJECT, 0, .u = { .prop_list = { tab, len } } } #define JS_ALIAS_DEF(name, from) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_ALIAS, 0, .u = { .alias = { from, -1 } } } #define JS_ALIAS_BASE_DEF(name, from, base) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_ALIAS, 0, .u = { .alias = { from, base } } } -void JS_SetPropertyFunctionList(JSContext *ctx, JSValueConst obj, - const JSCFunctionListEntry *tab, - int len); +int JS_SetPropertyFunctionList(JSContext *ctx, JSValueConst obj, + const JSCFunctionListEntry *tab, + int len); /* C module definition */ @@ -1112,12 +1137,14 @@ int JS_SetModuleExport(JSContext *ctx, JSModuleDef *m, const char *export_name, JSValue val); int JS_SetModuleExportList(JSContext *ctx, JSModuleDef *m, const JSCFunctionListEntry *tab, int len); - +/* associate a JSValue to a C module */ +int JS_SetModulePrivateValue(JSContext *ctx, JSModuleDef *m, JSValue val); +JSValue JS_GetModulePrivateValue(JSContext *ctx, JSModuleDef *m); + /* debug value output */ typedef struct { JS_BOOL show_hidden : 8; /* only show enumerable properties */ - JS_BOOL show_closure : 8; /* show closure variables */ JS_BOOL raw_dump : 8; /* avoid doing autoinit and avoid any malloc() call (for internal use) */ uint32_t max_depth; /* recurse up to this depth, 0 = no limit */ uint32_t max_string_length; /* print no more than this length for @@ -1126,9 +1153,13 @@ typedef struct { arrays or objects, 0 = no limit */ } JSPrintValueOptions; +typedef void JSPrintValueWrite(void *opaque, const char *buf, size_t len); + void JS_PrintValueSetDefaultOptions(JSPrintValueOptions *options); -void JS_PrintValueRT(JSRuntime *rt, FILE *fo, JSValueConst val, const JSPrintValueOptions *options); -void JS_PrintValue(JSContext *ctx, FILE *fo, JSValueConst val, const JSPrintValueOptions *options); +void JS_PrintValueRT(JSRuntime *rt, JSPrintValueWrite *write_func, void *write_opaque, + JSValueConst val, const JSPrintValueOptions *options); +void JS_PrintValue(JSContext *ctx, JSPrintValueWrite *write_func, void *write_opaque, + JSValueConst val, const JSPrintValueOptions *options); #undef js_unlikely #undef js_force_inline diff --git a/src/couch_quickjs/quickjs/run-test262.c b/src/couch_quickjs/quickjs/run-test262.c index d1fa4f30a9..100ed134a9 100644 --- a/src/couch_quickjs/quickjs/run-test262.c +++ b/src/couch_quickjs/quickjs/run-test262.c @@ -78,6 +78,7 @@ char *harness_dir; char *harness_exclude; char *harness_features; char *harness_skip_features; +int *harness_skip_features_count; char *error_filename; char *error_file; FILE *error_out; @@ -372,6 +373,12 @@ static void enumerate_tests(const char *path) namelist_cmp_indirect); } +static void js_print_value_write(void *opaque, const char *buf, size_t len) +{ + FILE *fo = opaque; + fwrite(buf, 1, len, fo); +} + static JSValue js_print(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -397,7 +404,7 @@ static JSValue js_print(JSContext *ctx, JSValueConst this_val, fwrite(str, 1, len, outfile); JS_FreeCString(ctx, str); } else { - JS_PrintValue(ctx, outfile, v, NULL); + JS_PrintValue(ctx, js_print_value_write, outfile, v, NULL); } } fputc('\n', outfile); @@ -490,8 +497,7 @@ static void *agent_start(void *arg) JS_FreeValue(ctx, ret_val); for(;;) { - JSContext *ctx1; - ret = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); + ret = JS_ExecutePendingJob(JS_GetRuntime(ctx), NULL); if (ret < 0) { js_std_dump_error(ctx); break; @@ -830,13 +836,21 @@ static char *load_file(const char *filename, size_t *lenp) return buf; } +static int json_module_init_test(JSContext *ctx, JSModuleDef *m) +{ + JSValue val; + val = JS_GetModulePrivateValue(ctx, m); + JS_SetModuleExport(ctx, m, "default", val); + return 0; +} + static JSModuleDef *js_module_loader_test(JSContext *ctx, - const char *module_name, void *opaque) + const char *module_name, void *opaque, + JSValueConst attributes) { size_t buf_len; uint8_t *buf; JSModuleDef *m; - JSValue func_val; char *filename, *slash, path[1024]; // interpret import("bar.js") from path/to/foo.js as @@ -858,15 +872,33 @@ static JSModuleDef *js_module_loader_test(JSContext *ctx, return NULL; } - /* compile the module */ - func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name, - JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); - js_free(ctx, buf); - if (JS_IsException(func_val)) - return NULL; - /* the module is already referenced, so we must free it */ - m = JS_VALUE_GET_PTR(func_val); - JS_FreeValue(ctx, func_val); + if (js_module_test_json(ctx, attributes) == 1) { + /* compile as JSON */ + JSValue val; + val = JS_ParseJSON(ctx, (char *)buf, buf_len, module_name); + js_free(ctx, buf); + if (JS_IsException(val)) + return NULL; + m = JS_NewCModule(ctx, module_name, json_module_init_test); + if (!m) { + JS_FreeValue(ctx, val); + return NULL; + } + /* only export the "default" symbol which will contain the JSON object */ + JS_AddModuleExport(ctx, m, "default"); + JS_SetModulePrivateValue(ctx, m, val); + } else { + JSValue func_val; + /* compile the module */ + func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name, + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + js_free(ctx, buf); + if (JS_IsException(func_val)) + return NULL; + /* the module is already referenced, so we must free it */ + m = JS_VALUE_GET_PTR(func_val); + JS_FreeValue(ctx, func_val); + } return m; } @@ -1238,8 +1270,7 @@ static int eval_buf(JSContext *ctx, const char *buf, size_t buf_len, JS_FreeValue(ctx, res_val); } for(;;) { - JSContext *ctx1; - ret = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); + ret = JS_ExecutePendingJob(JS_GetRuntime(ctx), NULL); if (ret < 0) { res_val = JS_EXCEPTION; break; @@ -1581,7 +1612,7 @@ int run_test_buf(const char *filename, const char *harness, namelist_t *ip, JS_SetCanBlock(rt, can_block); /* loader for ES6 modules */ - JS_SetModuleLoaderFunc(rt, NULL, js_module_loader_test, (void *)filename); + JS_SetModuleLoaderFunc2(rt, NULL, js_module_loader_test, NULL, (void *)filename); add_helpers(ctx); @@ -1706,10 +1737,13 @@ int run_test(const char *filename, int index) p = find_tag(desc, "features:", &state); if (p) { while ((option = get_option(&p, &state)) != NULL) { + char *p1; if (find_word(harness_features, option)) { /* feature is enabled */ - } else if (find_word(harness_skip_features, option)) { + } else if ((p1 = find_word(harness_skip_features, option)) != NULL) { /* skip disabled feature */ + if (harness_skip_features_count) + harness_skip_features_count[p1 - harness_skip_features]++; skip |= 1; } else { /* feature is not listed: skip and warn */ @@ -1882,7 +1916,7 @@ int run_test262_harness_test(const char *filename, BOOL is_module) JS_SetCanBlock(rt, can_block); /* loader for ES6 modules */ - JS_SetModuleLoaderFunc(rt, NULL, js_module_loader_test, (void *)filename); + JS_SetModuleLoaderFunc2(rt, NULL, js_module_loader_test, NULL, (void *)filename); add_helpers(ctx); @@ -1906,10 +1940,9 @@ int run_test262_harness_test(const char *filename, BOOL is_module) JS_FreeValue(ctx, res_val); } for(;;) { - JSContext *ctx1; - ret = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); + ret = JS_ExecutePendingJob(JS_GetRuntime(ctx), NULL); if (ret < 0) { - js_std_dump_error(ctx1); + js_std_dump_error(ctx); ret_code = 1; } else if (ret == 0) { break; @@ -2043,6 +2076,7 @@ int main(int argc, char **argv) const char *ignore = ""; BOOL is_test262_harness = FALSE; BOOL is_module = FALSE; + BOOL count_skipped_features = FALSE; clock_t clocks; #if !defined(_WIN32) @@ -2110,6 +2144,8 @@ int main(int argc, char **argv) is_test262_harness = TRUE; } else if (str_equal(arg, "--module")) { is_module = TRUE; + } else if (str_equal(arg, "--count_skipped_features")) { + count_skipped_features = TRUE; } else { fatal(1, "unknown option: %s", arg); break; @@ -2144,6 +2180,14 @@ int main(int argc, char **argv) clocks = clock(); + if (count_skipped_features) { + /* not storage efficient but it is simple */ + size_t size; + size = sizeof(harness_skip_features_count[0]) * strlen(harness_skip_features); + harness_skip_features_count = malloc(size); + memset(harness_skip_features_count, 0, size); + } + if (is_dir_list) { if (optind < argc && !isdigit((unsigned char)argv[optind][0])) { filename = argv[optind++]; @@ -2194,6 +2238,30 @@ int main(int argc, char **argv) printf("\n"); } + if (count_skipped_features) { + size_t i, n, len = strlen(harness_skip_features); + BOOL disp = FALSE; + int c; + for(i = 0; i < len; i++) { + if (harness_skip_features_count[i] != 0) { + if (!disp) { + disp = TRUE; + printf("%-30s %7s\n", "SKIPPED FEATURE", "COUNT"); + } + for(n = 0; n < 30; n++) { + c = harness_skip_features[i + n]; + if (is_word_sep(c)) + break; + putchar(c); + } + for(; n < 30; n++) + putchar(' '); + printf(" %7d\n", harness_skip_features_count[i]); + } + } + printf("\n"); + } + if (is_dir_list) { fprintf(stderr, "Result: %d/%d error%s", test_failed, test_count, test_count != 1 ? "s" : ""); @@ -2223,6 +2291,8 @@ int main(int argc, char **argv) namelist_free(&exclude_list); namelist_free(&exclude_dir_list); free(harness_dir); + free(harness_skip_features); + free(harness_skip_features_count); free(harness_features); free(harness_exclude); free(error_file); diff --git a/src/couch_quickjs/quickjs/test262.conf b/src/couch_quickjs/quickjs/test262.conf index 68906820ca..cf8b91d539 100644 --- a/src/couch_quickjs/quickjs/test262.conf +++ b/src/couch_quickjs/quickjs/test262.conf @@ -64,12 +64,12 @@ Array.prototype.flatMap Array.prototype.includes Array.prototype.values ArrayBuffer -arraybuffer-transfer=skip +arraybuffer-transfer arrow-function async-functions async-iteration Atomics -Atomics.pause=skip +Atomics.pause Atomics.waitAsync=skip BigInt caller @@ -103,12 +103,12 @@ destructuring-assignment destructuring-binding dynamic-import error-cause -Error.isError=skip +Error.isError explicit-resource-management=skip exponentiation export-star-as-namespace-from-module FinalizationRegistry -Float16Array=skip +Float16Array Float32Array Float64Array for-in-order @@ -116,9 +116,9 @@ for-of generators globalThis hashbang -host-gc-required=skip -import-assertions=skip -import-attributes=skip +host-gc-required +immutable-arraybuffer=skip +import-attributes import-defer=skip import.meta Int16Array @@ -142,17 +142,18 @@ Intl.NumberFormat-v3=skip Intl.RelativeTimeFormat=skip Intl.Segmenter=skip IsHTMLDDA -iterator-helpers=skip +iterator-helpers iterator-sequencing=skip -json-modules=skip +json-modules json-parse-with-source=skip json-superset legacy-regexp=skip let logical-assignment-operators Map -Math.sumPrecise=skip +Math.sumPrecise new.target +nonextensible-applies-to-private=skip numeric-separator-literal object-rest object-spread @@ -162,7 +163,7 @@ Object.is optional-catch-binding optional-chaining Promise -promise-try=skip +promise-try promise-with-resolvers Promise.allSettled Promise.any @@ -177,19 +178,21 @@ regexp-dotall regexp-duplicate-named-groups=skip regexp-lookbehind regexp-match-indices -regexp-modifiers=skip +regexp-modifiers regexp-named-groups regexp-unicode-property-escapes -regexp-v-flag=skip -RegExp.escape=skip -resizable-arraybuffer=skip +regexp-v-flag +RegExp.escape +resizable-arraybuffer rest-parameters Set -set-methods=skip +set-methods ShadowRealm=skip SharedArrayBuffer source-phase-imports-module-source=skip source-phase-imports=skip +stable-array-sort +stable-typedarray-sort string-trimming String.fromCodePoint String.prototype.at @@ -230,6 +233,7 @@ Uint32Array Uint8Array uint8array-base64=skip Uint8ClampedArray +upsert WeakMap WeakRef WeakSet @@ -250,32 +254,6 @@ test262/test/built-ins/ThrowTypeError/unique-per-realm-function-proto.js #test262/test/built-ins/RegExp/CharacterClassEscapes/ #test262/test/built-ins/RegExp/property-escapes/ -# feature regexp-v-flag is missing in the tests -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-digit-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-whitespace-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-negative-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-positive-cases.js -test262/test/built-ins/RegExp/CharacterClassEscapes/character-class-word-class-escape-positive-cases.js - # not yet in official specification test262/test/built-ins/String/prototype/match/cstm-matcher-on-bigint-primitive.js test262/test/built-ins/String/prototype/match/cstm-matcher-on-bigint-primitive.js @@ -331,24 +309,12 @@ test262/test/built-ins/String/prototype/split/cstm-split-on-string-primitive.js # spec updates it in this case) test262/test/staging/sm/Array/frozen-dense-array.js +# does not match spec +test262/test/staging/sm/Iterator/from/wrap-next-not-object-throws.js + # not supported -test262/test/staging/sm/Set/difference.js -test262/test/staging/sm/Set/intersection.js -test262/test/staging/sm/Set/is-disjoint-from.js -test262/test/staging/sm/Set/is-subset-of.js -test262/test/staging/sm/Set/is-superset-of.js -test262/test/staging/sm/Set/symmetric-difference.js -test262/test/staging/sm/Set/union.js test262/test/staging/sm/extensions/censor-strict-caller.js test262/test/staging/sm/JSON/parse-with-source.js -test262/test/staging/sm/RegExp/flags.js -test262/test/staging/sm/RegExp/prototype.js - -# no f16 -test262/test/staging/sm/Math/f16round.js -test262/test/staging/sm/TypedArray/sort_small.js -test262/test/staging/sm/extensions/dataview.js -test262/test/staging/sm/TypedArray/toString.js # not standard test262/test/staging/sm/Function/builtin-no-construct.js @@ -357,12 +323,25 @@ test262/test/staging/sm/Function/function-toString-builtin-name.js test262/test/staging/sm/extensions/arguments-property-access-in-function.js test262/test/staging/sm/extensions/function-caller-skips-eval-frames.js test262/test/staging/sm/extensions/function-properties.js +test262/test/staging/sm/regress/regress-577648-1.js +test262/test/staging/sm/regress/regress-577648-2.js +test262/test/staging/sm/regress/regress-584355.js +test262/test/staging/sm/regress/regress-586482-1.js +test262/test/staging/sm/regress/regress-586482-2.js +test262/test/staging/sm/regress/regress-586482-3.js +test262/test/staging/sm/regress/regress-586482-4.js +test262/test/staging/sm/regress/regress-699682.js + # RegExp toSource not fully compliant test262/test/staging/sm/RegExp/toString.js test262/test/staging/sm/RegExp/source.js test262/test/staging/sm/RegExp/escape.js +# RegExp.lastMatch not supported +test262/test/staging/sm/statements/regress-642975.js # source directives are not standard yet test262/test/staging/sm/syntax/syntax-parsed-arrow-then-directive.js +# returning "bound fn" as initialName for a function is permitted by the spec +test262/test/staging/sm/Function/function-toString-builtin.js [tests] # list test files or use config.testdir diff --git a/src/couch_quickjs/quickjs/test262_errors.txt b/src/couch_quickjs/quickjs/test262_errors.txt index 5fb832d6ba..cc49b2f322 100644 --- a/src/couch_quickjs/quickjs/test262_errors.txt +++ b/src/couch_quickjs/quickjs/test262_errors.txt @@ -1,67 +1,68 @@ -test262/test/language/module-code/top-level-await/module-graphs-does-not-hang.js:10: TypeError: $DONE() not called -test262/test/staging/sm/Date/UTC-convert-all-arguments.js:75: Test262Error: index 1: expected 42, got Error: didn't throw Expected SameValue(«Error: didn't throw», «42») to be true -test262/test/staging/sm/Date/constructor-convert-all-arguments.js:75: Test262Error: index undefined: expected 42, got Error: didn't throw Expected SameValue(«Error: didn't throw», «42») to be true -test262/test/staging/sm/Date/non-iso.js:76: Test262Error: Expected SameValue(«NaN», «-40071559730000») to be true -test262/test/staging/sm/Date/two-digit-years.js:76: Test262Error: Expected SameValue(«915177600000», «NaN») to be true -test262/test/staging/sm/Function/arguments-parameter-shadowing.js:15: Test262Error: Expected SameValue(«true», «false») to be true -test262/test/staging/sm/Function/constructor-binding.js:12: Test262Error: Expected SameValue(«"function"», «"undefined"») to be true -test262/test/staging/sm/Function/function-bind.js:14: Test262Error: Conforms to NativeFunction Syntax: "function bound unbound() {\n [native code]\n}" -test262/test/staging/sm/Function/function-name-for.js:12: Test262Error: Expected SameValue(«""», «"forInHead"») to be true -test262/test/staging/sm/Function/function-toString-builtin.js:14: Test262Error: Expected match to '/^\s*function\s*(get|set)?\s*(\w+|(?:'[^']*')|(?:"[^"]*")|\d+|(?:\[[^\]]+\]))?\s*\(\s*\)\s*\{\s*\[native code\]\s*\}\s*$/', Actual value 'function bound fn() { - [native code] -}' Expected SameValue(«null», «null») to be false -test262/test/staging/sm/Function/implicit-this-in-parameter-expression.js:13: Test262Error: Expected SameValue(«[object Object]», «undefined») to be true -test262/test/staging/sm/Function/invalid-parameter-list.js:35: Error: Assertion failed: expected exception SyntaxError, no exception thrown -test262/test/staging/sm/JSON/parse-number-syntax.js:39: Test262Error: parsing string <1.> threw a non-SyntaxError exception: Test262Error: string <1.> shouldn't have parsed as JSON Expected SameValue(«false», «true») to be true Expected SameValue(«true», «false») to be true -test262/test/staging/sm/JSON/parse-syntax-errors-02.js:51: Test262Error: parsing string <["Illegal backslash escape: \x15"]> threw a non-SyntaxError exception: Test262Error: string <["Illegal backslash escape: \x15"]> shouldn't have parsed as JSON Expected SameValue(«false», «true») to be true Expected SameValue(«true», «false») to be true -test262/test/staging/sm/Math/cbrt-approx.js:26: Error: got 1.39561242508609, expected a number near 1.3956124250860895 (relative error: 2) -test262/test/staging/sm/RegExp/constructor-ordering-2.js:15: Test262Error: Expected SameValue(«false», «true») to be true -test262/test/staging/sm/RegExp/match-trace.js:13: Test262Error: Expected SameValue(«"get:flags,get:unicode,set:lastIndex,get:exec,call:exec,get:result[0],get:exec,call:exec,get:result[0],get:exec,call:exec,"», «"get:flags,set:lastIndex,get:exec,call:exec,get:result[0],get:exec,call:exec,get:result[0],get:exec,call:exec,"») to be true -test262/test/staging/sm/RegExp/regress-613820-1.js:13: Test262Error: Expected SameValue(«"aaa"», «"aa"») to be true -test262/test/staging/sm/RegExp/regress-613820-2.js:13: Test262Error: Expected SameValue(«"f"», «undefined») to be true -test262/test/staging/sm/RegExp/regress-613820-3.js:13: Test262Error: Expected SameValue(«"aab"», «"aa"») to be true -test262/test/staging/sm/RegExp/replace-trace.js:13: Test262Error: Expected SameValue(«"get:flags,get:unicode,set:lastIndex,get:exec,call:exec,get:result[0],get:exec,call:exec,get:result[length],get:result[0],get:result[index],get:result[groups],"», «"get:flags,set:lastIndex,get:exec,call:exec,get:result[0],get:exec,call:exec,get:result[length],get:result[0],get:result[index],get:result[groups],"») to be true -test262/test/staging/sm/RegExp/unicode-ignoreCase-escape.js:22: Test262Error: Actual argument shouldn't be nullish. -test262/test/staging/sm/RegExp/unicode-ignoreCase-word-boundary.js:13: Test262Error: Expected SameValue(«false», «true») to be true -test262/test/staging/sm/String/match-defines-match-elements.js:52: Test262Error: Expected SameValue(«true», «false») to be true -test262/test/staging/sm/TypedArray/constructor-buffer-sequence.js:73: Error: Assertion failed: expected exception ExpectedError, got Error: Poisoned Value +test262/test/annexB/language/expressions/assignmenttargettype/callexpression-as-for-in-lhs.js:27: SyntaxError: invalid for in/of left hand-side +test262/test/annexB/language/expressions/assignmenttargettype/callexpression-as-for-of-lhs.js:27: SyntaxError: invalid for in/of left hand-side +test262/test/annexB/language/expressions/assignmenttargettype/callexpression-in-compound-assignment.js:33: SyntaxError: invalid assignment left-hand side +test262/test/annexB/language/expressions/assignmenttargettype/callexpression-in-postfix-update.js:27: SyntaxError: invalid increment/decrement operand +test262/test/annexB/language/expressions/assignmenttargettype/callexpression-in-prefix-update.js:27: SyntaxError: invalid increment/decrement operand +test262/test/annexB/language/expressions/assignmenttargettype/callexpression.js:33: SyntaxError: invalid assignment left-hand side +test262/test/annexB/language/expressions/assignmenttargettype/cover-callexpression-and-asyncarrowhead.js:20: SyntaxError: invalid assignment left-hand side +test262/test/language/expressions/assignment/S11.13.1_A6_T1.js:23: Test262Error: #1: innerX === undefined. Actual: 1 +test262/test/language/expressions/assignment/S11.13.1_A6_T2.js:23: Test262Error: #1: innerX === 2. Actual: 1 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.1_T1.js:24: Test262Error: #1: innerX === 2. Actual: 12 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.2_T1.js:24: Test262Error: #1: innerX === 2. Actual: 5 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.3_T1.js:24: Test262Error: #1: innerX === 2. Actual: 3 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.4_T1.js:24: Test262Error: #1: innerX === 2. Actual: 4 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.5_T1.js:24: Test262Error: #1: innerX === 2. Actual: 4 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.6_T1.js:24: Test262Error: #1: innerX === 2. Actual: 8 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.7_T1.js:24: Test262Error: #1: innerX === 2. Actual: 4 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.8_T1.js:24: Test262Error: #1: innerX === 2. Actual: 4 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.9_T1.js:24: Test262Error: #1: innerX === 2. Actual: 1 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.10_T1.js:24: Test262Error: #1: innerX === 2. Actual: 5 +test262/test/language/expressions/compound-assignment/S11.13.2_A6.11_T1.js:24: Test262Error: #1: innerX === 2. Actual: 5 +test262/test/language/identifier-resolution/assign-to-global-undefined.js:20: strict mode: expected error +test262/test/language/statements/expression/S12.4_A1.js:15: unexpected error type: Test262: This statement should not be evaluated. +test262/test/language/statements/expression/S12.4_A1.js:15: strict mode: unexpected error type: Test262: This statement should not be evaluated. +test262/test/staging/sm/Function/arguments-parameter-shadowing.js:14: Test262Error: Expected SameValue(«true», «false») to be true +test262/test/staging/sm/Function/constructor-binding.js:11: Test262Error: Expected SameValue(«"function"», «"undefined"») to be true +test262/test/staging/sm/Function/constructor-binding.js:11: strict mode: Test262Error: Expected SameValue(«"function"», «"undefined"») to be true +test262/test/staging/sm/Function/function-bind.js:24: Test262Error: Conforms to NativeFunction Syntax: "function bound unbound() {\n [native code]\n}" +test262/test/staging/sm/Function/function-name-for.js:13: Test262Error: Expected SameValue(«""», «"forInHead"») to be true +test262/test/staging/sm/Function/implicit-this-in-parameter-expression.js:12: Test262Error: Expected SameValue(«[object Object]», «undefined») to be true +test262/test/staging/sm/Function/invalid-parameter-list.js:13: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/Function/invalid-parameter-list.js:13: strict mode: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/RegExp/regress-613820-1.js:12: Test262Error: Actual [aaa, aa, a] and expected [aa, a, a] should have the same contents. +test262/test/staging/sm/RegExp/regress-613820-1.js:12: strict mode: Test262Error: Actual [aaa, aa, a] and expected [aa, a, a] should have the same contents. +test262/test/staging/sm/RegExp/regress-613820-2.js:12: Test262Error: Actual [foobar, f, o, o, b, a, r] and expected [foobar, undefined, undefined, undefined, b, a, r] should have the same contents. +test262/test/staging/sm/RegExp/regress-613820-2.js:12: strict mode: Test262Error: Actual [foobar, f, o, o, b, a, r] and expected [foobar, undefined, undefined, undefined, b, a, r] should have the same contents. +test262/test/staging/sm/RegExp/regress-613820-3.js:12: Test262Error: Actual [aab, a, undefined, ab] and expected [aa, undefined, a, undefined] should have the same contents. +test262/test/staging/sm/RegExp/regress-613820-3.js:12: strict mode: Test262Error: Actual [aab, a, undefined, ab] and expected [aa, undefined, a, undefined] should have the same contents. +test262/test/staging/sm/TypedArray/constructor-buffer-sequence.js:29: Test262Error: Expected a ExpectedError but got a Error +test262/test/staging/sm/TypedArray/constructor-buffer-sequence.js:29: strict mode: Test262Error: Expected a ExpectedError but got a Error test262/test/staging/sm/TypedArray/prototype-constructor-identity.js:17: Test262Error: Expected SameValue(«2», «6») to be true -test262/test/staging/sm/TypedArray/set-detached-bigint.js:27: Error: Assertion failed: expected exception SyntaxError, got RangeError: invalid array length -test262/test/staging/sm/TypedArray/set-detached.js:112: RangeError: invalid array length -test262/test/staging/sm/TypedArray/sort-negative-nan.js:102: TypeError: cannot read property 'name' of undefined -test262/test/staging/sm/TypedArray/sort_modifications.js:12: Test262Error: Int8Array at index 0 for size 4 Expected SameValue(«0», «1») to be true -test262/test/staging/sm/TypedArray/subarray.js:15: Test262Error: Expected SameValue(«0», «1») to be true -test262/test/staging/sm/async-functions/async-contains-unicode-escape.js:45: Error: Assertion failed: expected exception SyntaxError, no exception thrown -test262/test/staging/sm/async-functions/await-error.js:12: Test262Error: Expected SameValue(«false», «true») to be true -test262/test/staging/sm/async-functions/await-in-arrow-parameters.js:33: Error: Assertion failed: expected exception SyntaxError, no exception thrown - AsyncFunction:(a = (b = await/r/g) => {}) => {} -test262/test/staging/sm/class/boundFunctionSubclassing.js:12: Test262Error: Expected SameValue(«false», «true») to be true -test262/test/staging/sm/class/compPropNames.js:26: Error: Expected syntax error: ({[1, 2]: 3}) -test262/test/staging/sm/class/methDefn.js:26: Error: Expected syntax error: b = {a() => 0} -test262/test/staging/sm/class/strictExecution.js:32: Error: Assertion failed: expected exception TypeError, no exception thrown -test262/test/staging/sm/class/superPropOrdering.js:83: Error: Assertion failed: expected exception TypeError, no exception thrown -test262/test/staging/sm/expressions/optional-chain.js:25: Error: Assertion failed: expected exception SyntaxError, no exception thrown -test262/test/staging/sm/expressions/short-circuit-compound-assignment-const.js:97: TypeError: 'a' is read-only -test262/test/staging/sm/expressions/short-circuit-compound-assignment-tdz.js:23: Error: Assertion failed: expected exception ReferenceError, got TypeError: 'a' is read-only -test262/test/staging/sm/extensions/TypedArray-set-object-funky-length-detaches.js:55: RangeError: invalid array length -test262/test/staging/sm/extensions/regress-469625-01.js:16: Test262Error: TM: Array prototype and expression closures Expected SameValue(«"TypeError: [].__proto__ is not a function"», «"TypeError: not a function"») to be true -test262/test/staging/sm/generators/syntax.js:30: Error: Assertion failed: expected SyntaxError, but no exception thrown - function* g() { (function* yield() {}); } -test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-arguments.js:14: Test262Error: Expected SameValue(«"object"», «"function"») to be true -test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-eval.js:12: Test262Error: Expected SameValue(«"outer-gouter-geval-gtruefalseq"», «"outer-geval-gwith-gtruefalseq"») to be true -test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-if.js:20: TypeError: not a function -test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-notapplicable.js:15: Test262Error: Expected SameValue(«function x() {2}», «function x() {1}») to be true +test262/test/staging/sm/TypedArray/prototype-constructor-identity.js:17: strict mode: Test262Error: Expected SameValue(«2», «6») to be true +test262/test/staging/sm/TypedArray/sort_modifications.js:9: Test262Error: Int8Array at index 0 for size 4 Expected SameValue(«0», «1») to be true +test262/test/staging/sm/TypedArray/sort_modifications.js:9: strict mode: Test262Error: Int8Array at index 0 for size 4 Expected SameValue(«0», «1») to be true +test262/test/staging/sm/async-functions/async-contains-unicode-escape.js:11: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/async-functions/async-contains-unicode-escape.js:11: strict mode: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/async-functions/await-in-arrow-parameters.js:10: Test262Error: AsyncFunction:(a = (b = await/r/g) => {}) => {} Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/async-functions/await-in-arrow-parameters.js:10: strict mode: Test262Error: AsyncFunction:(a = (b = await/r/g) => {}) => {} Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/class/boundFunctionSubclassing.js:9: Test262Error: Expected SameValue(«false», «true») to be true +test262/test/staging/sm/class/boundFunctionSubclassing.js:9: strict mode: Test262Error: Expected SameValue(«false», «true») to be true +test262/test/staging/sm/class/strictExecution.js:13: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all +test262/test/staging/sm/class/superPropOrdering.js:17: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all +test262/test/staging/sm/class/superPropOrdering.js:17: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all +test262/test/staging/sm/generators/syntax.js:50: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-arguments.js:13: Test262Error: Expected SameValue(«"object"», «"function"») to be true +test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-eval.js:11: Test262Error: Expected SameValue(«"outer-gouter-geval-gtruefalseq"», «"outer-geval-gwith-gtruefalseq"») to be true +test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-if.js:19: TypeError: not a function +test262/test/staging/sm/lexical-environment/block-scoped-functions-annex-b-notapplicable.js:14: Test262Error: Expected SameValue(«function x() {2}», «function x() {1}») to be true test262/test/staging/sm/lexical-environment/block-scoped-functions-deprecated-redecl.js:23: Test262Error: Expected SameValue(«3», «4») to be true -test262/test/staging/sm/lexical-environment/var-in-catch-body-annex-b-eval.js:17: Test262Error: Expected SameValue(«"g"», «"global-x"») to be true -test262/test/staging/sm/object/defineProperties-order.js:14: Test262Error: Expected SameValue(«"ownKeys,getOwnPropertyDescriptor,getOwnPropertyDescriptor,get,get"», «"ownKeys,getOwnPropertyDescriptor,get,getOwnPropertyDescriptor,get"») to be true -test262/test/staging/sm/regress/regress-577648-1.js:21: Test262Error: 1 Expected SameValue(«true», «false») to be true -test262/test/staging/sm/regress/regress-577648-2.js:14: Test262Error: Expected SameValue(«true», «false») to be true -test262/test/staging/sm/regress/regress-584355.js:12: Test262Error: Expected SameValue(«"function f () { ff (); }"», «"undefined"») to be true -test262/test/staging/sm/regress/regress-586482-1.js:19: Test262Error: ok Expected SameValue(«true», «false») to be true -test262/test/staging/sm/regress/regress-586482-2.js:19: Test262Error: ok Expected SameValue(«true», «false») to be true -test262/test/staging/sm/regress/regress-586482-3.js:18: Test262Error: ok Expected SameValue(«true», «false») to be true -test262/test/staging/sm/regress/regress-586482-4.js:14: Test262Error: ok Expected SameValue(«function() { this.f(); }», «undefined») to be true -test262/test/staging/sm/regress/regress-602621.js:14: Test262Error: function sub-statement must override arguments Expected SameValue(«"function"», «"object"») to be true -test262/test/staging/sm/regress/regress-699682.js:15: Test262Error: Expected SameValue(«false», «true») to be true -test262/test/staging/sm/regress/regress-1383630.js:30: Error: Assertion failed: expected exception TypeError, no exception thrown -test262/test/staging/sm/statements/arrow-function-in-for-statement-head.js:15: Test262Error: expected syntax error, got Error: didn't throw Expected SameValue(«false», «true») to be true -test262/test/staging/sm/statements/regress-642975.js:14: Test262Error: Expected SameValue(«undefined», «"y"») to be true -test262/test/staging/sm/statements/try-completion.js:17: Test262Error: Expected SameValue(«"try"», «undefined») to be true +test262/test/staging/sm/lexical-environment/var-in-catch-body-annex-b-eval.js:16: Test262Error: Expected SameValue(«"g"», «"global-x"») to be true +test262/test/staging/sm/object/defineProperties-order.js:11: Test262Error: Expected SameValue(«"ownKeys,getOwnPropertyDescriptor,getOwnPropertyDescriptor,get,get"», «"ownKeys,getOwnPropertyDescriptor,get,getOwnPropertyDescriptor,get"») to be true +test262/test/staging/sm/object/defineProperties-order.js:11: strict mode: Test262Error: Expected SameValue(«"ownKeys,getOwnPropertyDescriptor,getOwnPropertyDescriptor,get,get"», «"ownKeys,getOwnPropertyDescriptor,get,getOwnPropertyDescriptor,get"») to be true +test262/test/staging/sm/regress/regress-602621.js:13: Test262Error: function sub-statement must override arguments Expected SameValue(«"function"», «"object"») to be true +test262/test/staging/sm/regress/regress-1383630.js:28: Test262Error: proxy must report the same value for the non-writable, non-configurable property Expected a TypeError to be thrown but no exception was thrown at all +test262/test/staging/sm/regress/regress-1383630.js:28: strict mode: Test262Error: proxy must report the same value for the non-writable, non-configurable property Expected a TypeError to be thrown but no exception was thrown at all +test262/test/staging/sm/statements/arrow-function-in-for-statement-head.js:13: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/statements/arrow-function-in-for-statement-head.js:13: strict mode: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all +test262/test/staging/sm/statements/try-completion.js:11: Test262Error: Expected SameValue(«"try"», «undefined») to be true +test262/test/staging/sm/statements/try-completion.js:11: strict mode: Test262Error: Expected SameValue(«"try"», «undefined») to be true diff --git a/src/couch_quickjs/quickjs/tests/test262.patch b/src/couch_quickjs/quickjs/tests/test262.patch index b6f4aa5ece..d7cba88cc0 100644 --- a/src/couch_quickjs/quickjs/tests/test262.patch +++ b/src/couch_quickjs/quickjs/tests/test262.patch @@ -1,5 +1,5 @@ diff --git a/harness/atomicsHelper.js b/harness/atomicsHelper.js -index 9828b15..4a5919d 100644 +index 9828b15..9e24d64 100644 --- a/harness/atomicsHelper.js +++ b/harness/atomicsHelper.js @@ -272,10 +272,14 @@ $262.agent.waitUntil = function(typedArray, index, expected) { @@ -14,9 +14,9 @@ index 9828b15..4a5919d 100644 +// small: 200, +// long: 1000, +// huge: 10000, -+ yield: 20, -+ small: 20, -+ long: 100, ++ yield: 40, ++ small: 40, ++ long: 200, + huge: 1000, }; @@ -70,36 +70,3 @@ index b397be0..c197ddc 100644 } return result; } -diff --git a/harness/sm/non262.js b/harness/sm/non262.js -index c1829e3..3a3ee27 100644 ---- a/harness/sm/non262.js -+++ b/harness/sm/non262.js -@@ -41,8 +41,6 @@ globalThis.createNewGlobal = function() { - return $262.createRealm().global - } - --function print(...args) { --} - function assertEq(...args) { - assert.sameValue(...args) - } -@@ -71,4 +69,4 @@ if (globalThis.createExternalArrayBuffer === undefined) { - if (globalThis.enableGeckoProfilingWithSlowAssertions === undefined) { - globalThis.enableGeckoProfilingWithSlowAssertions = globalThis.enableGeckoProfiling = - globalThis.disableGeckoProfiling = () => {} --} -\ No newline at end of file -+} -diff --git a/test/staging/sm/misc/new-with-non-constructor.js b/test/staging/sm/misc/new-with-non-constructor.js -index 18c2f0c..f9aa209 100644 ---- a/test/staging/sm/misc/new-with-non-constructor.js -+++ b/test/staging/sm/misc/new-with-non-constructor.js -@@ -16,7 +16,7 @@ function checkConstruct(thing) { - new thing(); - assert.sameValue(0, 1, "not reached " + thing); - } catch (e) { -- assert.sameValue(e.message.includes(" is not a constructor") || -+ assert.sameValue(e.message.includes("not a constructor") || - e.message === "Function.prototype.toString called on incompatible object", true); - } - } diff --git a/src/couch_quickjs/rebar.config.script b/src/couch_quickjs/rebar.config.script index 60ab64070b..f6546e9997 100644 --- a/src/couch_quickjs/rebar.config.script +++ b/src/couch_quickjs/rebar.config.script @@ -15,8 +15,13 @@ % Windows is "special" so we treat it "specially" Msys = "msys2_shell.cmd -defterm -no-start -ucrt64 -here -lc ". +% Disabled CONFIG_LTO=y as of June 2025. It interfered with -j8 and microbench +% on quickjs repo didn't show any measurable per benefit for it. It even looked +% a bit slower on ubuntu-noble/x86_64 (lto:9032ns vs nolto:8746ns) and only a +% tiny bit faster on MacOS x86_64 (lto:12646 vs nolto:12614) +% PreHooks = [ - {"(linux|darwin)", compile, "make CONFIG_LTO=y -C quickjs -j8 libquickjs.lto.a qjsc"}, + {"(linux|darwin)", compile, "make -C quickjs -j8 libquickjs.a qjsc"}, {"freebsd", compile, "gmake -C quickjs -j8 libquickjs.a qjsc"}, {"win32", compile, Msys ++ "'make -C quickjs -j8 libquickjs.a qjsc.exe'"}, {"(linux|darwin|freebsd|win32)", compile, "escript build_js.escript compile"} @@ -38,14 +43,9 @@ ResetFlags = [ {"EXE_LDFLAGS", ""} ]. -LinuxDarwinEnv = [ - {"CFLAGS", "$CFLAGS -flto -g -Wall -DCONFIG_LTO=y -O2 -Iquickjs"}, - {"LDFLAGS", "$LDFLAGS -flto -lm quickjs/libquickjs.lto.a"} -] ++ ResetFlags. - -FreeBSDEnv = [ +UnixEnv = [ {"CFLAGS", "$CFLAGS -g -Wall -O2 -Iquickjs"}, - {"LDFLAGS", "$LDFLAGS -lm -lpthread quickjs/libquickjs.a"} + {"LDFLAGS", "$LDFLAGS quickjs/libquickjs.a -lpthread -lm"} ] ++ ResetFlags. WindowsEnv = [ @@ -73,10 +73,8 @@ WindowsMainjsSrc = WindowsBaseSrc ++ UnixMainjsSrc. WindowsCoffeeSrc = WindowsBaseSrc ++ UnixCoffeeSrc. PortSpecs = [ - {"(linux|darwin)", "priv/couchjs_mainjs", UnixMainjsSrc, [{env, LinuxDarwinEnv}]}, - {"(linux|darwin)", "priv/couchjs_coffee", UnixCoffeeSrc, [{env, LinuxDarwinEnv}]}, - {"freebsd", "priv/couchjs_mainjs", UnixMainjsSrc, [{env, FreeBSDEnv}]}, - {"freebsd", "priv/couchjs_coffee", UnixCoffeeSrc, [{env, FreeBSDEnv}]}, + {"(linux|darwin|freebsd)", "priv/couchjs_mainjs", UnixMainjsSrc, [{env, UnixEnv}]}, + {"(linux|darwin|freebsd)", "priv/couchjs_coffee", UnixCoffeeSrc, [{env, UnixEnv}]}, {"win32", "priv/couchjs_mainjs.exe", WindowsMainjsSrc, [{env, WindowsEnv}]}, {"win32", "priv/couchjs_coffee.exe", WindowsCoffeeSrc, [{env, WindowsEnv}]} ]. diff --git a/src/couch_quickjs/src/couch_quickjs_scanner_plugin.erl b/src/couch_quickjs/src/couch_quickjs_scanner_plugin.erl index 0d7b233de4..91385010c3 100644 --- a/src/couch_quickjs/src/couch_quickjs_scanner_plugin.erl +++ b/src/couch_quickjs/src/couch_quickjs_scanner_plugin.erl @@ -23,6 +23,7 @@ shards/2, db_opened/2, doc_id/3, + doc_fdi/3, doc/3, db_closing/2 ]). @@ -149,7 +150,7 @@ db_opened(#st{} = St, Db) -> #st{max_docs = MaxDocs, max_step = MaxStep} = St, {ok, DocTotal} = couch_db:get_doc_count(Db), Step = min(MaxStep, max(1, DocTotal div MaxDocs)), - {ok, St#st{doc_cnt = 0, doc_step = Step, docs = []}}. + {0, [], St#st{doc_cnt = 0, doc_step = Step, docs = []}}. doc_id(#st{} = St, <>, _Db) -> {skip, St}; @@ -162,6 +163,12 @@ doc_id(#st{doc_cnt = C, doc_step = S} = St, _DocId, _Db) when C rem S /= 0 -> doc_id(#st{doc_cnt = C} = St, _DocId, _Db) -> {ok, St#st{doc_cnt = C + 1}}. +doc_fdi(#st{} = St, #full_doc_info{deleted = true}, _Db) -> + % Skip deleted; don't even open the doc body + {stop, St}; +doc_fdi(#st{} = St, #full_doc_info{}, _Db) -> + {ok, St}. + doc(#st{} = St, Db, #doc{id = DocId} = Doc) -> #st{sid = SId} = St, JsonDoc = couch_query_servers:json_doc(Doc), diff --git a/src/couch_quickjs/update_and_apply_patches.sh b/src/couch_quickjs/update.sh similarity index 88% rename from src/couch_quickjs/update_and_apply_patches.sh rename to src/couch_quickjs/update.sh index 92fbb63550..87454c9c3d 100755 --- a/src/couch_quickjs/update_and_apply_patches.sh +++ b/src/couch_quickjs/update.sh @@ -7,9 +7,6 @@ set -e # This is the main branch of the github mirror URL=https://github.com/bellard/quickjs/archive/refs/heads/master.zip # -# The other alternatives: -# https://github.com/quickjs-ng/quickjs/commits/master/ - echo echo " * backup quickjs to quickjs.bak" @@ -50,6 +47,9 @@ echo " * removing quickjs.bak" rm -rf quickjs.bak echo -# Example how to generate patches: +# Example how to update patches themselves: # +# Run +# ./update_patches.sh +# OR manually run after cloning and unzipping master.zip from quickjs: # diff -u quickjs-master/quickjs.c quickjs/quickjs.c > patches/01-spidermonkey-185-mode.patch diff --git a/src/couch_quickjs/update_patches.sh b/src/couch_quickjs/update_patches.sh new file mode 100755 index 0000000000..f56a0e0c06 --- /dev/null +++ b/src/couch_quickjs/update_patches.sh @@ -0,0 +1,28 @@ +# Update patches +# +# Call this script after using update_and_apply_patches.sh to adjust +# the patches themselves. Sometimes line offsets drift and so this takes +# a new diff from the master QuickJS vs current source tree and regenerates +# the patch. + +set -e + +URL=https://github.com/bellard/quickjs/archive/refs/heads/master.zip + +echo " * wget ${URL}" +rm -rf master.zip quickjs-master +wget -q ${URL} +echo " * unzip master.zip to quickjs-master" +unzip -q -o master.zip + +set +e + +echo " * updating 01-spidermonkey-185-mode.patch" +diff -u quickjs-master/quickjs.c quickjs/quickjs.c > patches/01-spidermonkey-185-mode.patch + +echo " * updating 02-test262-errors.patch" +diff -u quickjs-master/test262_errors.txt quickjs/test262_errors.txt> patches/02-test262-errors.patch +set -e + +echo " * cleaning up" +rm -rf master.zip quickjs-master diff --git a/src/couch_replicator/src/couch_replicator_api_wrap.erl b/src/couch_replicator/src/couch_replicator_api_wrap.erl index ce22ceb64b..9364757d6c 100644 --- a/src/couch_replicator/src/couch_replicator_api_wrap.erl +++ b/src/couch_replicator/src/couch_replicator_api_wrap.erl @@ -115,10 +115,10 @@ db_open(#httpdb{} = Db1, Create, CreateParams) -> throw(Error); error:Error -> db_close(Db), - erlang:error(Error); + error(Error); exit:Error -> db_close(Db), - erlang:exit(Error) + exit(Error) end. db_close(#httpdb{httpc_pool = Pool} = HttpDb) -> @@ -300,7 +300,7 @@ open_doc_revs(#httpdb{} = HttpDb, Id, Revs, Options, Fun, Acc) -> % hammer approach to making sure it releases % that connection back to the pool. spawn(fun() -> - Ref = erlang:monitor(process, Self), + Ref = monitor(process, Self), receive {'DOWN', Ref, process, Self, normal} -> exit(Streamer, {streamer_parent_died, Self}); @@ -352,7 +352,7 @@ open_doc_revs(#httpdb{} = HttpDb, Id, Revs, Options, Fun, Acc) -> NewRetries = Retries - 1, case NewRetries > 0 of true -> - Wait = 2 * erlang:min(Wait0 * 2, ?MAX_WAIT), + Wait = 2 * min(Wait0 * 2, ?MAX_WAIT), LogRetryMsg = "Retrying GET to ~s in ~p seconds due to error ~w", couch_log:notice(LogRetryMsg, [Url, Wait / 1000, error_reason(Else)]), ok = timer:sleep(Wait), @@ -532,7 +532,7 @@ changes_since( UserFun, Options ) -> - Timeout = erlang:max(1000, InactiveTimeout div 3), + Timeout = max(1000, InactiveTimeout div 3), EncodedSeq = couch_replicator_utils:seq_encode(StartSeq), BaseQArgs = case get_value(continuous, Options, false) of @@ -820,7 +820,7 @@ run_user_fun(UserFun, Arg, UserAcc, OldRef) -> end), receive {started_open_doc_revs, NewRef} -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), exit(Pid, kill), restart_remote_open_doc_revs(OldRef, NewRef); {'DOWN', Ref, process, Pid, {exit_ok, Ret}} -> @@ -828,9 +828,9 @@ run_user_fun(UserFun, Arg, UserAcc, OldRef) -> {'DOWN', Ref, process, Pid, {exit_throw, Reason}} -> throw(Reason); {'DOWN', Ref, process, Pid, {exit_error, Reason}} -> - erlang:error(Reason); + error(Reason); {'DOWN', Ref, process, Pid, {exit_exit, Reason}} -> - erlang:exit(Reason) + exit(Reason) end. restart_remote_open_doc_revs(Ref, NewRef) -> @@ -844,7 +844,7 @@ restart_remote_open_doc_revs(Ref, NewRef) -> {headers, Ref, _} -> restart_remote_open_doc_revs(Ref, NewRef) after 0 -> - erlang:error({restart_open_doc_revs, NewRef}) + error({restart_open_doc_revs, NewRef}) end. remote_open_doc_revs_streamer_start(Parent) -> diff --git a/src/couch_replicator/src/couch_replicator_auth_session.erl b/src/couch_replicator/src/couch_replicator_auth_session.erl index 182e3cc865..316ce015ee 100644 --- a/src/couch_replicator/src/couch_replicator_auth_session.erl +++ b/src/couch_replicator/src/couch_replicator_auth_session.erl @@ -62,7 +62,7 @@ handle_call/3, handle_cast/2, handle_info/2, - format_status/2 + format_status/1 ]). -include_lib("ibrowse/include/ibrowse.hrl"). @@ -154,13 +154,21 @@ handle_info(Msg, State) -> couch_log:error("~p : Received un-expected message ~p", [?MODULE, Msg]), {noreply, State}. -format_status(_Opt, [_PDict, State]) -> - [ - {epoch, State#state.epoch}, - {user, State#state.user}, - {session_url, State#state.session_url}, - {refresh_tstamp, State#state.refresh_tstamp} - ]. +format_status(Status) -> + maps:map( + fun + (state, State) -> + #{ + epoch => State#state.epoch, + user => State#state.user, + session_url => State#state.session_url, + refresh_tstamp => State#state.refresh_tstamp + }; + (_, Value) -> + Value + end, + Status + ). %% Private helper functions @@ -336,7 +344,7 @@ stop_worker_if_server_requested(ResultHeaders0, Worker) -> ResultHeaders = mochiweb_headers:make(ResultHeaders0), case mochiweb_headers:get_value("Connection", ResultHeaders) of "close" -> - Ref = erlang:monitor(process, Worker), + Ref = monitor(process, Worker), ibrowse_http_client:stop(Worker), receive {'DOWN', Ref, _, _, _} -> @@ -379,7 +387,7 @@ parse_cookie(Headers) -> {error, cookie_not_found}; [_ | _] = Cookies -> case get_auth_session_cookies_and_age(Cookies) of - [] -> {error, cookie_format_invalid}; + [] -> {error, cookie_not_found}; [{Cookie, MaxAge} | _] -> {ok, MaxAge, Cookie} end end. @@ -800,4 +808,14 @@ get_auth_session_cookies_and_age_test() -> ]) ). +parse_cookie_test() -> + NotFound = {error, cookie_not_found}, + ?assertEqual(NotFound, parse_cookie([])), + ?assertEqual(NotFound, parse_cookie([{"abc", "def"}])), + ?assertEqual(NotFound, parse_cookie([{"set-cookiee", "c=v"}])), + ?assertEqual(NotFound, parse_cookie([{"set-cookie", ""}])), + ?assertEqual(NotFound, parse_cookie([{"Set-cOokie", "c=v"}])), + ?assertEqual({ok, undefined, "x"}, parse_cookie([{"set-cookie", "authsession=x"}])), + ?assertEqual({ok, 4, "x"}, parse_cookie([{"set-cookie", "authsession=x; max-age=4"}])). + -endif. diff --git a/src/couch_replicator/src/couch_replicator_connection.erl b/src/couch_replicator/src/couch_replicator_connection.erl index 978962bd76..110c792bb8 100644 --- a/src/couch_replicator/src/couch_replicator_connection.erl +++ b/src/couch_replicator/src/couch_replicator_connection.erl @@ -77,6 +77,9 @@ init([]) -> {inactivity_timeout, Interval}, {worker_trap_exits, false} ]), + % Try loading all the OS CA certs to give users an early indication in the + % logs if there is an error. + couch_replicator_utils:cacert_get(), {ok, #state{close_interval = Interval, timer = Timer}}. acquire(Url) -> diff --git a/src/couch_replicator/src/couch_replicator_doc_processor_worker.erl b/src/couch_replicator/src/couch_replicator_doc_processor_worker.erl index b9468080c6..5a62b3e06d 100644 --- a/src/couch_replicator/src/couch_replicator_doc_processor_worker.erl +++ b/src/couch_replicator/src/couch_replicator_doc_processor_worker.erl @@ -66,7 +66,7 @@ worker_fun(Id, Rep, WaitSec, WRef) -> {'DOWN', Ref, _, Pid, Result} -> exit(#doc_worker_result{id = Id, wref = WRef, result = Result}) after ?WORKER_TIMEOUT_MSEC -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), exit(Pid, kill), {DbName, DocId} = Id, TimeoutSec = round(?WORKER_TIMEOUT_MSEC / 1000), diff --git a/src/couch_replicator/src/couch_replicator_docs.erl b/src/couch_replicator/src/couch_replicator_docs.erl index 6b324c97a4..d28ae908cd 100644 --- a/src/couch_replicator/src/couch_replicator_docs.erl +++ b/src/couch_replicator/src/couch_replicator_docs.erl @@ -159,7 +159,7 @@ update_rep_doc(RepDbName, RepDocId, KVs, Wait) when is_binary(RepDocId) -> throw:conflict -> Msg = "Conflict when updating replication doc `~s`. Retrying.", couch_log:error(Msg, [RepDocId]), - ok = timer:sleep(rand:uniform(erlang:min(128, Wait)) * 100), + ok = timer:sleep(rand:uniform(min(128, Wait)) * 100), update_rep_doc(RepDbName, RepDocId, KVs, Wait * 2) end; update_rep_doc(RepDbName, #doc{body = {RepDocBody}} = RepDoc, KVs, _Try) -> diff --git a/src/couch_replicator/src/couch_replicator_fabric.erl b/src/couch_replicator/src/couch_replicator_fabric.erl index cb441fea71..9f6d945be3 100644 --- a/src/couch_replicator/src/couch_replicator_fabric.erl +++ b/src/couch_replicator/src/couch_replicator_fabric.erl @@ -121,7 +121,7 @@ handle_message({meta, Meta0}, {Worker, From}, State) -> offset = Offset }}; false -> - FinalOffset = erlang:min(Total, Offset + State#collector.skip), + FinalOffset = min(Total, Offset + State#collector.skip), Meta = [{total, Total}, {offset, FinalOffset}], {Go, Acc} = Callback({meta, Meta}, AccIn), {Go, State#collector{ diff --git a/src/couch_replicator/src/couch_replicator_fabric_rpc.erl b/src/couch_replicator/src/couch_replicator_fabric_rpc.erl index 1a3582928e..680a758b88 100644 --- a/src/couch_replicator/src/couch_replicator_fabric_rpc.erl +++ b/src/couch_replicator/src/couch_replicator_fabric_rpc.erl @@ -106,7 +106,7 @@ docs_test_() -> fun() -> ok end, fun(_) -> ok end, [ - ?TDEF_FE(t_docs) + ?TDEF_FE(t_docs, 15) ] }. @@ -140,10 +140,10 @@ docs_cb_test_() -> end, fun(_) -> meck:unload() end, [ - ?TDEF_FE(t_docs_cb_meta), - ?TDEF_FE(t_docs_cb_row_skip), - ?TDEF_FE(t_docs_cb_row), - ?TDEF_FE(t_docs_cb_complete) + ?TDEF_FE(t_docs_cb_meta, 15), + ?TDEF_FE(t_docs_cb_row_skip, 15), + ?TDEF_FE(t_docs_cb_row, 15), + ?TDEF_FE(t_docs_cb_complete, 15) ] }. diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl index cd5e4d75df..fe81e65ea0 100644 --- a/src/couch_replicator/src/couch_replicator_httpc.erl +++ b/src/couch_replicator/src/couch_replicator_httpc.erl @@ -38,7 +38,7 @@ % consuming the request. This threshold gives us confidence we'll % continue to properly close changes feeds while avoiding any case % where we may end up processing an unbounded number of messages. --define(MAX_DISCARDED_MESSAGES, 16). +-define(MAX_DISCARDED_MESSAGES, 100). setup(Db) -> #httpdb{ @@ -154,7 +154,7 @@ send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) -> %% {error, req_timedout} error. While in reality is not really a timeout, just %% a race condition. stop_and_release_worker(Pool, Worker) -> - Ref = erlang:monitor(process, Worker), + Ref = monitor(process, Worker), ibrowse_http_client:stop(Worker), receive {'DOWN', Ref, _, _, _} -> @@ -230,7 +230,7 @@ process_stream_response(ReqId, Worker, HttpDb, Params, Callback) -> Ok =:= 413 -> put(?STOP_HTTP_WORKER, stop); true -> ok end, - ibrowse:stream_next(ReqId), + ok = ibrowse:stream_next(ReqId), try Ret = Callback(Ok, Headers, StreamDataFun), Ret @@ -297,12 +297,16 @@ clean_mailbox({ibrowse_req_id, ReqId}, Count) when Count > 0 -> discard_message(ReqId, Worker, Count); false -> put(?STREAM_STATUS, ended), - ok + % Worker is not alive but we may still messages + % in the mailbox from it so recurse to clean them + clean_mailbox({ibrowse_req_id, ReqId}, Count) end; Status when Status == init; Status == ended -> receive {ibrowse_async_response, ReqId, _} -> clean_mailbox({ibrowse_req_id, ReqId}, Count - 1); + {ibrowse_async_response_timeout, ReqId} -> + clean_mailbox({ibrowse_req_id, ReqId}, Count - 1); {ibrowse_async_response_end, ReqId} -> put(?STREAM_STATUS, ended), ok @@ -314,16 +318,26 @@ clean_mailbox(_, Count) when Count > 0 -> ok. discard_message(ReqId, Worker, Count) -> - ibrowse:stream_next(ReqId), - receive - {ibrowse_async_response, ReqId, _} -> - clean_mailbox({ibrowse_req_id, ReqId}, Count - 1); - {ibrowse_async_response_end, ReqId} -> + case ibrowse:stream_next(ReqId) of + ok -> + receive + {ibrowse_async_response, ReqId, _} -> + clean_mailbox({ibrowse_req_id, ReqId}, Count - 1); + {ibrowse_async_response_timeout, ReqId} -> + clean_mailbox({ibrowse_req_id, ReqId}, Count - 1); + {ibrowse_async_response_end, ReqId} -> + put(?STREAM_STATUS, ended), + ok + after 30000 -> + exit(Worker, {timeout, ibrowse_stream_cleanup}), + exit({timeout, ibrowse_stream_cleanup}) + end; + {error, unknown_req_id} -> + % The stream is being torn down so expect to handle stream ids not + % being found. We don't want to sleep for 30 seconds and then exit. + % Just clean any left-over mailbox messages and move on. put(?STREAM_STATUS, ended), - ok - after 30000 -> - exit(Worker, {timeout, ibrowse_stream_cleanup}), - exit({timeout, ibrowse_stream_cleanup}) + clean_mailbox({ibrowse_req_id, ReqId}, Count) end. -spec maybe_retry(any(), pid(), #httpdb{}, list()) -> no_return(). @@ -341,7 +355,7 @@ maybe_retry( false -> ok = timer:sleep(Wait), log_retry_error(Params, HttpDb, Wait, Error), - Wait2 = erlang:min(Wait * 2, ?MAX_WAIT), + Wait2 = min(Wait * 2, ?MAX_WAIT), HttpDb1 = HttpDb#httpdb{retries = Retries - 1, wait = Wait2}, HttpDb2 = update_first_error_timestamp(HttpDb1), throw({retry, HttpDb2, Params}) @@ -403,7 +417,7 @@ error_cause(Cause) -> stream_data_self(#httpdb{timeout = T} = HttpDb, Params, Worker, ReqId, Cb) -> case accumulate_messages(ReqId, [], T + 500) of {Data, ibrowse_async_response} -> - ibrowse:stream_next(ReqId), + ok = ibrowse:stream_next(ReqId), {Data, fun() -> stream_data_self(HttpDb, Params, Worker, ReqId, Cb) end}; {Data, ibrowse_async_response_end} -> put(?STREAM_STATUS, ended), @@ -540,4 +554,125 @@ merge_headers_test() -> ?assertEqual([{"a", "y"}], merge_headers([{"A", "z"}, {"a", "y"}], [])), ?assertEqual([{"a", "y"}], merge_headers([], [{"A", "z"}, {"a", "y"}])). +clean_mailbox_test_() -> + { + foreach, + fun setup/0, + fun teardown/1, + [ + ?TDEF_FE(t_clean_noop), + ?TDEF_FE(t_clean_skip_other_messages), + ?TDEF_FE(t_clean_when_init), + ?TDEF_FE(t_clean_when_ended), + ?TDEF_FE(t_clean_when_streaming), + ?TDEF_FE(t_clean_when_streaming_dead_pid), + ?TDEF_FE(t_other_req_id_is_ignored) + ] + }. + +setup() -> + meck:new(ibrowse), + meck:expect(ibrowse, stream_next, 1, ok), + ok. + +teardown(_) -> + meck:unload(). + +t_clean_noop(_) -> + ReqId = make_ref(), + ?assertEqual(ok, clean_mailbox(random_junk)), + meck:expect(ibrowse, stream_next, 1, {error, unknown_req_id}), + set_stream_status({streaming, self()}), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})), + set_stream_status(init), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})), + set_stream_status(ended), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})). + +t_clean_skip_other_messages(_) -> + set_stream_status(init), + self() ! other_message, + ReqId = make_ref(), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})), + ?assertEqual([other_message], flush()). + +t_clean_when_init(_) -> + set_stream_status(init), + ReqId = make_ref(), + add_all_message_types(ReqId), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})), + ?assertEqual([], flush()), + ?assertEqual(ended, stream_status()). + +t_clean_when_ended(_) -> + set_stream_status(init), + ReqId = make_ref(), + add_all_message_types(ReqId), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})), + ?assertEqual([], flush()), + ?assertEqual(ended, stream_status()). + +t_clean_when_streaming(_) -> + set_stream_status({streaming, self()}), + ReqId = make_ref(), + add_all_message_types(ReqId), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})), + ?assertEqual([], flush()), + ?assertEqual(ended, stream_status()). + +t_clean_when_streaming_dead_pid(_) -> + {Pid, Ref} = spawn_monitor(fun() -> ok end), + receive + {'DOWN', Ref, _, _, _} -> ok + end, + set_stream_status({streaming, Pid}), + ReqId = make_ref(), + add_all_message_types(ReqId), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId})), + ?assertEqual([], flush()), + ?assertEqual(ended, stream_status()). + +t_other_req_id_is_ignored(_) -> + set_stream_status({streaming, self()}), + ReqId1 = make_ref(), + add_all_message_types(ReqId1), + ReqId2 = make_ref(), + add_all_message_types(ReqId2), + ?assertEqual(ok, clean_mailbox({ibrowse_req_id, ReqId1})), + ?assertEqual( + [ + {ibrowse_async_response, ReqId2, foo}, + {ibrowse_async_response_timeout, ReqId2}, + {ibrowse_async_response_end, ReqId2} + ], + flush() + ), + ?assertEqual(ended, stream_status()). + +stream_status() -> + get(?STREAM_STATUS). + +set_stream_status(Status) -> + put(?STREAM_STATUS, Status). + +add_all_message_types(ReqId) -> + Messages = [ + {ibrowse_async_response, ReqId, foo}, + {ibrowse_async_response_timeout, ReqId}, + {ibrowse_async_response_end, ReqId} + ], + [self() ! M || M <- Messages], + ok. + +flush() -> + flush([]). + +flush(Acc) -> + receive + Msg -> + flush([Msg | Acc]) + after 0 -> + lists:reverse(Acc) + end. + -endif. diff --git a/src/couch_replicator/src/couch_replicator_httpc_pool.erl b/src/couch_replicator/src/couch_replicator_httpc_pool.erl index fb15dcea16..3c2f6edfa2 100644 --- a/src/couch_replicator/src/couch_replicator_httpc_pool.erl +++ b/src/couch_replicator/src/couch_replicator_httpc_pool.erl @@ -19,7 +19,7 @@ % gen_server API -export([init/1, handle_call/3, handle_info/2, handle_cast/2]). --export([format_status/2]). +-export([format_status/1]). -include_lib("couch/include/couch_db.hrl"). @@ -135,27 +135,31 @@ handle_info({'DOWN', Ref, process, _, _}, #state{callers = Callers} = State) -> {noreply, State} end. -format_status(_Opt, [_PDict, State]) -> - #state{ - url = Url, - proxy_url = ProxyUrl - } = State, - [ - {data, [ - {"State", State#state{ - url = couch_util:url_strip_password(Url), - proxy_url = couch_util:url_strip_password(ProxyUrl) - }} - ]} - ]. +format_status(Status) -> + maps:map( + fun + (state, State) -> + #state{ + url = Url, + proxy_url = ProxyUrl + } = State, + State#state{ + url = couch_util:url_strip_password(Url), + proxy_url = couch_util:url_strip_password(ProxyUrl) + }; + (_, Value) -> + Value + end, + Status + ). monitor_client(Callers, Worker, {ClientPid, _}) -> - [{Worker, erlang:monitor(process, ClientPid)} | Callers]. + [{Worker, monitor(process, ClientPid)} | Callers]. demonitor_client(Callers, Worker) -> case lists:keysearch(Worker, 1, Callers) of {value, {Worker, MonRef}} -> - erlang:demonitor(MonRef, [flush]), + demonitor(MonRef, [flush]), lists:keydelete(Worker, 1, Callers); false -> Callers @@ -196,13 +200,16 @@ release_worker_internal(Worker, State) -> format_status_test_() -> ?_test(begin - State = #state{ - url = "https://username1:password1@$ACCOUNT2.cloudant.com/db", - proxy_url = "https://username2:password2@proxy.thing.com:8080/" + Status = #{ + state => + #state{ + url = "https://username1:password1@$ACCOUNT2.cloudant.com/db", + proxy_url = "https://username2:password2@proxy.thing.com:8080/" + } }, - [{data, [{"State", ScrubbedN}]}] = format_status(normal, [[], State]), - ?assertEqual("https://username1:*****@$ACCOUNT2.cloudant.com/db", ScrubbedN#state.url), - ?assertEqual("https://username2:*****@proxy.thing.com:8080/", ScrubbedN#state.proxy_url), + #{state := State} = format_status(Status), + ?assertEqual("https://username1:*****@$ACCOUNT2.cloudant.com/db", State#state.url), + ?assertEqual("https://username2:*****@proxy.thing.com:8080/", State#state.proxy_url), ok end). diff --git a/src/couch_replicator/src/couch_replicator_notifier.erl b/src/couch_replicator/src/couch_replicator_notifier.erl index 21c6d5a25c..4b33e0d06a 100644 --- a/src/couch_replicator/src/couch_replicator_notifier.erl +++ b/src/couch_replicator/src/couch_replicator_notifier.erl @@ -14,6 +14,8 @@ -behaviour(gen_event). +-define(NAME, couch_replication). + % public API -export([start_link/1, stop/1, notify/1]). @@ -21,17 +23,20 @@ -export([init/1]). -export([handle_event/2, handle_call/2, handle_info/2]). --include_lib("couch/include/couch_db.hrl"). - start_link(FunAcc) -> - couch_event_sup:start_link( - couch_replication, - {couch_replicator_notifier, make_ref()}, - FunAcc - ). + couch_event_sup:start_link(?NAME, {?MODULE, make_ref()}, FunAcc). notify(Event) -> - gen_event:notify(couch_replication, Event). + try + gen_event:notify(?NAME, Event) + catch + _:_ -> + % It's possible some jobs may remain around after the notification + % service had shut down or crashed. Avoid making a mess in the logs + % and just ignore that. At that point nobody will notice the + % notification anyway. + ok + end. stop(Pid) -> couch_event_sup:stop(Pid). @@ -51,3 +56,12 @@ handle_call(_Msg, State) -> handle_info(_Msg, State) -> {ok, State}. + +-ifdef(TEST). + +-include_lib("couch/include/couch_eunit.hrl"). + +couch_replicator_notify_when_stopped_test() -> + ?assertEqual(ok, notify({stopped, foo})). + +-endif. diff --git a/src/couch_replicator/src/couch_replicator_parse.erl b/src/couch_replicator/src/couch_replicator_parse.erl index b72e1f5765..5f8437992b 100644 --- a/src/couch_replicator/src/couch_replicator_parse.erl +++ b/src/couch_replicator/src/couch_replicator_parse.erl @@ -488,15 +488,28 @@ ssl_params(Url) -> -spec ssl_verify_options(true | false) -> [_]. ssl_verify_options(true) -> - CAFile = cfg("ssl_trusted_certificates_file"), - [ - {verify, verify_peer}, - {customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]}, - {cacertfile, CAFile} - ]; + % https://security.erlef.org/secure_coding_and_deployment_hardening/ssl.html + ssl_ca_cert_opts() ++ + [ + {verify, verify_peer}, + {customize_hostname_check, [ + {match_fun, public_key:pkix_verify_hostname_match_fun(https)} + ]} + ]; ssl_verify_options(false) -> [{verify, verify_none}]. +ssl_ca_cert_opts() -> + % Try to use the CA cert file from config first, and if not specified, use + % the CA certificates from the OS. If those can't be loaded either, then + % crash: cacerts_get/0 raises an error in that case and we do not catch it. + case cfg("ssl_trusted_certificates_file") of + undefined -> + [{cacerts, public_key:cacerts_get()}]; + CAFile when is_list(CAFile) -> + [{cacertfile, CAFile}] + end. + get_value(Key, Props) -> couch_util:get_value(Key, Props). diff --git a/src/couch_replicator/src/couch_replicator_pg.erl b/src/couch_replicator/src/couch_replicator_pg.erl index 25937ec15f..5f4f3bba78 100644 --- a/src/couch_replicator/src/couch_replicator_pg.erl +++ b/src/couch_replicator/src/couch_replicator_pg.erl @@ -47,7 +47,14 @@ join({_, _} = RepId, Pid) when is_pid(Pid) -> % quicker. % leave({_, _} = RepId, Pid) when is_pid(Pid) -> - pg:leave(?MODULE, id(RepId), Pid). + try + pg:leave(?MODULE, id(RepId), Pid) + catch + _:_ -> + ok + % If this is called during shutdown the pg gen_server might be + % gone. So we avoid blocking on it or making a mess in the logs + end. % Determine if a replication job should start on a particular node. If it % should, return `yes`, otherwise return `{no, OtherPid}`. `OtherPid` is @@ -150,4 +157,9 @@ t_should_run(_) -> ok = join(RepId, InitPid), ?assertEqual({no, InitPid}, should_run(RepId, Pid)). +couch_replicator_pg_test_leave_when_stopped_test() -> + RepId = {"a", "+b"}, + Pid = self(), + ?assertEqual(ok, leave(RepId, Pid)). + -endif. diff --git a/src/couch_replicator/src/couch_replicator_rate_limiter.erl b/src/couch_replicator/src/couch_replicator_rate_limiter.erl index c98f98d614..ccffd90114 100644 --- a/src/couch_replicator/src/couch_replicator_rate_limiter.erl +++ b/src/couch_replicator/src/couch_replicator_rate_limiter.erl @@ -170,9 +170,9 @@ update_failure(_Key, Interval, Timestamp, Now) when % Ignore too frequent updates. Interval; update_failure(Key, Interval, _Timestamp, Now) -> - Interval1 = erlang:max(Interval, ?BASE_INTERVAL), + Interval1 = max(Interval, ?BASE_INTERVAL), Interval2 = round(Interval1 * ?BACKOFF_FACTOR), - Interval3 = erlang:min(Interval2, ?MAX_INTERVAL), + Interval3 = min(Interval2, ?MAX_INTERVAL), insert(Key, Interval3, Now). -spec insert(any(), interval(), msec()) -> interval(). @@ -195,7 +195,7 @@ interval_and_timestamp(Key) -> -spec time_decay(msec(), interval()) -> interval(). time_decay(Dt, Interval) when Dt > ?TIME_DECAY_THRESHOLD -> DecayedInterval = Interval - ?TIME_DECAY_FACTOR * Dt, - erlang:max(round(DecayedInterval), 0); + max(round(DecayedInterval), 0); time_decay(_Dt, Interval) -> Interval. diff --git a/src/couch_replicator/src/couch_replicator_scheduler.erl b/src/couch_replicator/src/couch_replicator_scheduler.erl index a3aa7601d2..aabd7febde 100644 --- a/src/couch_replicator/src/couch_replicator_scheduler.erl +++ b/src/couch_replicator/src/couch_replicator_scheduler.erl @@ -25,7 +25,7 @@ handle_call/3, handle_info/2, handle_cast/2, - format_status/2 + format_status/1 ]). -export([ @@ -71,6 +71,13 @@ -define(DEFAULT_MAX_HISTORY, 20). -define(DEFAULT_SCHEDULER_INTERVAL, 60000). +% Worker children get a default 5 second shutdown timeout, so pick a value just +% a bit less than that: 4.5 seconds. In couch_replicator_sup our scheduler +% worker doesn't specify the timeout, so it up picks ups the OTP default of 5 +% seconds https://www.erlang.org/doc/system/sup_princ.html#child-specification +% +-define(TERMINATE_SHUTDOWN_TIME, 4500). + -record(state, { interval = ?DEFAULT_SCHEDULER_INTERVAL, timer, @@ -346,15 +353,24 @@ handle_info(_, State) -> {noreply, State}. terminate(_Reason, _State) -> + stop_clear_all_jobs(?TERMINATE_SHUTDOWN_TIME), couch_replicator_share:clear(), ok. -format_status(_Opt, [_PDict, State]) -> - [ - {max_jobs, State#state.max_jobs}, - {running_jobs, running_job_count()}, - {pending_jobs, pending_job_count()} - ]. +format_status(Status) -> + maps:map( + fun + (state, State) -> + #{ + max_jobs => State#state.max_jobs, + running_jobs => running_job_count(), + pending_jobs => pending_job_count() + }; + (_, Value) -> + Value + end, + Status + ). %% config listener functions @@ -387,6 +403,33 @@ handle_config_terminate(_, _, _) -> %% Private functions +% Stop jobs in parallel +% +stop_clear_all_jobs(TimeLeftMSec) -> + ShutdownFun = fun(#job{pid = Pid}) -> + Pid ! shutdown, + monitor(process, Pid) + end, + Refs = lists:map(ShutdownFun, running_jobs()), + ets:delete_all_objects(?MODULE), + wait_jobs_stop(TimeLeftMSec, Refs). + +wait_jobs_stop(_, []) -> + ok; +wait_jobs_stop(TimeLeftMSec, _) when TimeLeftMSec =< 0 -> + % If some survive a bit longer we let them finish checkpointing. + timeout; +wait_jobs_stop(TimeLeftMsec, [Ref | Refs]) -> + T0 = erlang:monotonic_time(), + receive + {'DOWN', Ref, _, _, _} -> + Dt = erlang:monotonic_time() - T0, + DtMSec = erlang:convert_time_unit(Dt, native, millisecond), + wait_jobs_stop(TimeLeftMsec - DtMSec, Refs) + after TimeLeftMsec -> + ok + end. + % Handle crashed jobs. Handling differs between transient and permanent jobs. % Transient jobs are those posted to the _replicate endpoint. They don't have a % db associated with them. When those jobs crash, they are not restarted. That @@ -594,7 +637,7 @@ backoff_micros(CrashCount) -> % exponent in Base * 2 ^ CrashCount to achieve an exponential backoff % doubling every consecutive failure, starting with the base value of % ?BACKOFF_INTERVAL_MICROS. - BackoffExp = erlang:min(CrashCount - 1, ?MAX_BACKOFF_EXPONENT), + BackoffExp = min(CrashCount - 1, ?MAX_BACKOFF_EXPONENT), (1 bsl BackoffExp) * ?BACKOFF_INTERVAL_MICROS. -spec add_job_int(#job{}) -> boolean(). @@ -895,7 +938,7 @@ update_running_jobs_stats(StatsPid) when is_pid(StatsPid) -> ok. start_stats_updater() -> - erlang:spawn_link(?MODULE, stats_updater_loop, [undefined]). + spawn_link(?MODULE, stats_updater_loop, [undefined]). stats_updater_loop(Timer) -> receive @@ -908,7 +951,7 @@ stats_updater_loop(Timer) -> ok = stats_updater_refresh(), ?MODULE:stats_updater_loop(undefined); Else -> - erlang:exit({stats_updater_bad_msg, Else}) + exit({stats_updater_bad_msg, Else}) end. -spec stats_updater_refresh() -> ok. @@ -946,7 +989,7 @@ existing_replication(#rep{} = NewRep) -> -ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). +-include_lib("couch/include/couch_eunit.hrl"). backoff_micros_test_() -> BaseInterval = ?BACKOFF_INTERVAL_MICROS, @@ -1045,464 +1088,412 @@ scheduler_test_() -> fun setup/0, fun teardown/1, [ - t_pending_jobs_simple(), - t_pending_jobs_skip_crashed(), - t_pending_jobs_skip_running(), - t_one_job_starts(), - t_no_jobs_start_if_max_is_0(), - t_one_job_starts_if_max_is_1(), - t_max_churn_does_not_throttle_initial_start(), - t_excess_oneshot_only_jobs(), - t_excess_continuous_only_jobs(), - t_excess_prefer_continuous_first(), - t_stop_oldest_first(), - t_start_oldest_first(), - t_jobs_churn_even_if_not_all_max_jobs_are_running(), - t_jobs_dont_churn_if_there_are_available_running_slots(), - t_start_only_pending_jobs_do_not_churn_existing_ones(), - t_dont_stop_if_nothing_pending(), - t_max_churn_limits_number_of_rotated_jobs(), - t_existing_jobs(), - t_if_pending_less_than_running_start_all_pending(), - t_running_less_than_pending_swap_all_running(), - t_oneshot_dont_get_rotated(), - t_rotate_continuous_only_if_mixed(), - t_oneshot_dont_get_starting_priority(), - t_oneshot_will_hog_the_scheduler(), - t_if_excess_is_trimmed_rotation_still_happens(), - t_if_transient_job_crashes_it_gets_removed(), - t_if_permanent_job_crashes_it_stays_in_ets(), - t_job_summary_running(), - t_job_summary_pending(), - t_job_summary_crashing_once(), - t_job_summary_crashing_many_times(), - t_job_summary_proxy_fields() + ?TDEF_FE(t_pending_jobs_simple), + ?TDEF_FE(t_pending_jobs_skip_crashed), + ?TDEF_FE(t_pending_jobs_skip_running), + ?TDEF_FE(t_one_job_starts), + ?TDEF_FE(t_no_jobs_start_if_max_is_0), + ?TDEF_FE(t_one_job_starts_if_max_is_1), + ?TDEF_FE(t_max_churn_does_not_throttle_initial_start), + ?TDEF_FE(t_excess_oneshot_only_jobs), + ?TDEF_FE(t_excess_continuous_only_jobs), + ?TDEF_FE(t_excess_prefer_continuous_first), + ?TDEF_FE(t_stop_oldest_first), + ?TDEF_FE(t_start_oldest_first), + ?TDEF_FE(t_jobs_churn_even_if_not_all_max_jobs_are_running), + ?TDEF_FE(t_jobs_dont_churn_if_there_are_available_running_slots), + ?TDEF_FE(t_start_only_pending_jobs_do_not_churn_existing_ones), + ?TDEF_FE(t_dont_stop_if_nothing_pending), + ?TDEF_FE(t_max_churn_limits_number_of_rotated_jobs), + ?TDEF_FE(t_existing_jobs), + ?TDEF_FE(t_if_pending_less_than_running_start_all_pending), + ?TDEF_FE(t_running_less_than_pending_swap_all_running), + ?TDEF_FE(t_oneshot_dont_get_rotated), + ?TDEF_FE(t_rotate_continuous_only_if_mixed), + ?TDEF_FE(t_oneshot_dont_get_starting_priority), + ?TDEF_FE(t_oneshot_will_hog_the_scheduler), + ?TDEF_FE(t_if_excess_is_trimmed_rotation_still_happens), + ?TDEF_FE(t_if_transient_job_crashes_it_gets_removed), + ?TDEF_FE(t_if_permanent_job_crashes_it_stays_in_ets), + ?TDEF_FE(t_stop_all_stops_jobs), + ?TDEF_FE(t_job_summary_running), + ?TDEF_FE(t_job_summary_pending), + ?TDEF_FE(t_job_summary_crashing_once), + ?TDEF_FE(t_job_summary_crashing_many_times), + ?TDEF_FE(t_job_summary_proxy_fields) ] } }. -t_pending_jobs_simple() -> - ?_test(begin - Job1 = oneshot(1), - Job2 = oneshot(2), - setup_jobs([Job2, Job1]), - ?assertEqual([], pending_jobs(0)), - ?assertEqual([Job1], pending_jobs(1)), - ?assertEqual([Job1, Job2], pending_jobs(2)), - ?assertEqual([Job1, Job2], pending_jobs(3)) - end). - -t_pending_jobs_skip_crashed() -> - ?_test(begin - Job = oneshot(1), - Ts = os:timestamp(), - History = [crashed(Ts), started(Ts) | Job#job.history], - Job1 = Job#job{history = History}, - Job2 = oneshot(2), - Job3 = oneshot(3), - setup_jobs([Job2, Job1, Job3]), - ?assertEqual([Job2], pending_jobs(1)), - ?assertEqual([Job2, Job3], pending_jobs(2)), - ?assertEqual([Job2, Job3], pending_jobs(3)) - end). - -t_pending_jobs_skip_running() -> - ?_test(begin - Job1 = continuous(1), - Job2 = continuous_running(2), - Job3 = oneshot(3), - Job4 = oneshot_running(4), - Jobs = [Job1, Job2, Job3, Job4], - setup_jobs(Jobs), - ?assertEqual([Job1, Job3], pending_jobs(4)) - end). - -t_one_job_starts() -> - ?_test(begin - setup_jobs([oneshot(1)]), - ?assertEqual({0, 1}, run_stop_count()), - reschedule(mock_state(?DEFAULT_MAX_JOBS)), - ?assertEqual({1, 0}, run_stop_count()) - end). - -t_no_jobs_start_if_max_is_0() -> - ?_test(begin - setup_jobs([oneshot(1)]), - reschedule(mock_state(0)), - ?assertEqual({0, 1}, run_stop_count()) - end). - -t_one_job_starts_if_max_is_1() -> - ?_test(begin - setup_jobs([oneshot(1), oneshot(2)]), - reschedule(mock_state(1)), - ?assertEqual({1, 1}, run_stop_count()) - end). - -t_max_churn_does_not_throttle_initial_start() -> - ?_test(begin - setup_jobs([oneshot(1), oneshot(2)]), - reschedule(mock_state(?DEFAULT_MAX_JOBS, 0)), - ?assertEqual({2, 0}, run_stop_count()) - end). - -t_excess_oneshot_only_jobs() -> - ?_test(begin - setup_jobs([oneshot_running(1), oneshot_running(2)]), - ?assertEqual({2, 0}, run_stop_count()), - reschedule(mock_state(1)), - ?assertEqual({1, 1}, run_stop_count()), - reschedule(mock_state(0)), - ?assertEqual({0, 2}, run_stop_count()) - end). - -t_excess_continuous_only_jobs() -> - ?_test(begin - setup_jobs([continuous_running(1), continuous_running(2)]), - ?assertEqual({2, 0}, run_stop_count()), - reschedule(mock_state(1)), - ?assertEqual({1, 1}, run_stop_count()), - reschedule(mock_state(0)), - ?assertEqual({0, 2}, run_stop_count()) - end). - -t_excess_prefer_continuous_first() -> - ?_test(begin - Jobs = [ - continuous_running(1), - oneshot_running(2), - continuous_running(3) - ], - setup_jobs(Jobs), - ?assertEqual({3, 0}, run_stop_count()), - ?assertEqual({1, 0}, oneshot_run_stop_count()), - reschedule(mock_state(2)), - ?assertEqual({2, 1}, run_stop_count()), - ?assertEqual({1, 0}, oneshot_run_stop_count()), - reschedule(mock_state(1)), - ?assertEqual({1, 0}, oneshot_run_stop_count()), - reschedule(mock_state(0)), - ?assertEqual({0, 1}, oneshot_run_stop_count()) - end). - -t_stop_oldest_first() -> - ?_test(begin - Jobs = [ - continuous_running(7), - continuous_running(4), - continuous_running(5) - ], - setup_jobs(Jobs), - reschedule(mock_state(2, 1)), - ?assertEqual({2, 1}, run_stop_count()), - ?assertEqual([4], jobs_stopped()), - reschedule(mock_state(1, 1)), - ?assertEqual([7], jobs_running()) - end). - -t_start_oldest_first() -> - ?_test(begin - setup_jobs([continuous(7), continuous(2), continuous(5)]), - reschedule(mock_state(1)), - ?assertEqual({1, 2}, run_stop_count()), - ?assertEqual([2], jobs_running()), - reschedule(mock_state(2)), - ?assertEqual({2, 1}, run_stop_count()), - % After rescheduling with max_jobs = 2, 2 was stopped and 5, 7 should - % be running. - ?assertEqual([2], jobs_stopped()) - end). - -t_jobs_churn_even_if_not_all_max_jobs_are_running() -> - ?_test(begin - setup_jobs([ - continuous_running(7), - continuous(2), - continuous(5) - ]), - reschedule(mock_state(2, 2)), - ?assertEqual({2, 1}, run_stop_count()), - ?assertEqual([7], jobs_stopped()) - end). - -t_jobs_dont_churn_if_there_are_available_running_slots() -> - ?_test(begin - setup_jobs([ - continuous_running(1), - continuous_running(2) - ]), - reschedule(mock_state(2, 2)), - ?assertEqual({2, 0}, run_stop_count()), - ?assertEqual([], jobs_stopped()), - ?assertEqual(0, meck:num_calls(couch_replicator_scheduler_job, start_link, 1)) - end). - -t_start_only_pending_jobs_do_not_churn_existing_ones() -> - ?_test(begin - setup_jobs([ - continuous(1), - continuous_running(2) - ]), - reschedule(mock_state(2, 2)), - ?assertEqual(1, meck:num_calls(couch_replicator_scheduler_job, start_link, 1)), - ?assertEqual([], jobs_stopped()), - ?assertEqual({2, 0}, run_stop_count()) - end). - -t_dont_stop_if_nothing_pending() -> - ?_test(begin - setup_jobs([continuous_running(1), continuous_running(2)]), - reschedule(mock_state(2)), - ?assertEqual({2, 0}, run_stop_count()) - end). - -t_max_churn_limits_number_of_rotated_jobs() -> - ?_test(begin - Jobs = [ - continuous(1), - continuous_running(2), - continuous(3), - continuous_running(4) - ], - setup_jobs(Jobs), - reschedule(mock_state(2, 1)), - ?assertEqual([2, 3], jobs_stopped()) - end). - -t_if_pending_less_than_running_start_all_pending() -> - ?_test(begin - Jobs = [ - continuous(1), - continuous_running(2), - continuous(3), - continuous_running(4), - continuous_running(5) - ], - setup_jobs(Jobs), - reschedule(mock_state(3)), - ?assertEqual([1, 2, 5], jobs_running()) - end). - -t_running_less_than_pending_swap_all_running() -> - ?_test(begin - Jobs = [ - continuous(1), - continuous(2), - continuous(3), - continuous_running(4), - continuous_running(5) - ], - setup_jobs(Jobs), - reschedule(mock_state(2)), - ?assertEqual([3, 4, 5], jobs_stopped()) - end). - -t_oneshot_dont_get_rotated() -> - ?_test(begin - setup_jobs([oneshot_running(1), continuous(2)]), - reschedule(mock_state(1)), - ?assertEqual([1], jobs_running()) - end). - -t_rotate_continuous_only_if_mixed() -> - ?_test(begin - setup_jobs([continuous(1), oneshot_running(2), continuous_running(3)]), - reschedule(mock_state(2)), - ?assertEqual([1, 2], jobs_running()) - end). - -t_oneshot_dont_get_starting_priority() -> - ?_test(begin - setup_jobs([continuous(1), oneshot(2), continuous_running(3)]), - reschedule(mock_state(1)), - ?assertEqual([1], jobs_running()) - end). +t_pending_jobs_simple(_) -> + Job1 = oneshot(1), + Job2 = oneshot(2), + setup_jobs([Job2, Job1]), + ?assertEqual([], pending_jobs(0)), + ?assertEqual([Job1], pending_jobs(1)), + ?assertEqual([Job1, Job2], pending_jobs(2)), + ?assertEqual([Job1, Job2], pending_jobs(3)). + +t_pending_jobs_skip_crashed(_) -> + Job = oneshot(1), + Ts = os:timestamp(), + History = [crashed(Ts), started(Ts) | Job#job.history], + Job1 = Job#job{history = History}, + Job2 = oneshot(2), + Job3 = oneshot(3), + setup_jobs([Job2, Job1, Job3]), + ?assertEqual([Job2], pending_jobs(1)), + ?assertEqual([Job2, Job3], pending_jobs(2)), + ?assertEqual([Job2, Job3], pending_jobs(3)). + +t_pending_jobs_skip_running(_) -> + Job1 = continuous(1), + Job2 = continuous_running(2), + Job3 = oneshot(3), + Job4 = oneshot_running(4), + Jobs = [Job1, Job2, Job3, Job4], + setup_jobs(Jobs), + ?assertEqual([Job1, Job3], pending_jobs(4)). + +t_one_job_starts(_) -> + setup_jobs([oneshot(1)]), + ?assertEqual({0, 1}, run_stop_count()), + reschedule(mock_state(?DEFAULT_MAX_JOBS)), + ?assertEqual({1, 0}, run_stop_count()). + +t_no_jobs_start_if_max_is_0(_) -> + setup_jobs([oneshot(1)]), + reschedule(mock_state(0)), + ?assertEqual({0, 1}, run_stop_count()). + +t_one_job_starts_if_max_is_1(_) -> + setup_jobs([oneshot(1), oneshot(2)]), + reschedule(mock_state(1)), + ?assertEqual({1, 1}, run_stop_count()). + +t_max_churn_does_not_throttle_initial_start(_) -> + setup_jobs([oneshot(1), oneshot(2)]), + reschedule(mock_state(?DEFAULT_MAX_JOBS, 0)), + ?assertEqual({2, 0}, run_stop_count()). + +t_excess_oneshot_only_jobs(_) -> + setup_jobs([oneshot_running(1), oneshot_running(2)]), + ?assertEqual({2, 0}, run_stop_count()), + reschedule(mock_state(1)), + ?assertEqual({1, 1}, run_stop_count()), + reschedule(mock_state(0)), + ?assertEqual({0, 2}, run_stop_count()). + +t_excess_continuous_only_jobs(_) -> + setup_jobs([continuous_running(1), continuous_running(2)]), + ?assertEqual({2, 0}, run_stop_count()), + reschedule(mock_state(1)), + ?assertEqual({1, 1}, run_stop_count()), + reschedule(mock_state(0)), + ?assertEqual({0, 2}, run_stop_count()). + +t_excess_prefer_continuous_first(_) -> + Jobs = [ + continuous_running(1), + oneshot_running(2), + continuous_running(3) + ], + setup_jobs(Jobs), + ?assertEqual({3, 0}, run_stop_count()), + ?assertEqual({1, 0}, oneshot_run_stop_count()), + reschedule(mock_state(2)), + ?assertEqual({2, 1}, run_stop_count()), + ?assertEqual({1, 0}, oneshot_run_stop_count()), + reschedule(mock_state(1)), + ?assertEqual({1, 0}, oneshot_run_stop_count()), + reschedule(mock_state(0)), + ?assertEqual({0, 1}, oneshot_run_stop_count()). + +t_stop_oldest_first(_) -> + Jobs = [ + continuous_running(7), + continuous_running(4), + continuous_running(5) + ], + setup_jobs(Jobs), + reschedule(mock_state(2, 1)), + ?assertEqual({2, 1}, run_stop_count()), + ?assertEqual([4], jobs_stopped()), + reschedule(mock_state(1, 1)), + ?assertEqual([7], jobs_running()). + +t_start_oldest_first(_) -> + setup_jobs([continuous(7), continuous(2), continuous(5)]), + reschedule(mock_state(1)), + ?assertEqual({1, 2}, run_stop_count()), + ?assertEqual([2], jobs_running()), + reschedule(mock_state(2)), + ?assertEqual({2, 1}, run_stop_count()), + % After rescheduling with max_jobs = 2, 2 was stopped and 5, 7 should + % be running. + ?assertEqual([2], jobs_stopped()). + +t_jobs_churn_even_if_not_all_max_jobs_are_running(_) -> + setup_jobs([ + continuous_running(7), + continuous(2), + continuous(5) + ]), + reschedule(mock_state(2, 2)), + ?assertEqual({2, 1}, run_stop_count()), + ?assertEqual([7], jobs_stopped()). + +t_jobs_dont_churn_if_there_are_available_running_slots(_) -> + setup_jobs([ + continuous_running(1), + continuous_running(2) + ]), + reschedule(mock_state(2, 2)), + ?assertEqual({2, 0}, run_stop_count()), + ?assertEqual([], jobs_stopped()), + ?assertEqual(0, meck:num_calls(couch_replicator_scheduler_job, start_link, 1)). + +t_start_only_pending_jobs_do_not_churn_existing_ones(_) -> + setup_jobs([ + continuous(1), + continuous_running(2) + ]), + reschedule(mock_state(2, 2)), + ?assertEqual(1, meck:num_calls(couch_replicator_scheduler_job, start_link, 1)), + ?assertEqual([], jobs_stopped()), + ?assertEqual({2, 0}, run_stop_count()). + +t_dont_stop_if_nothing_pending(_) -> + setup_jobs([continuous_running(1), continuous_running(2)]), + reschedule(mock_state(2)), + ?assertEqual({2, 0}, run_stop_count()). + +t_max_churn_limits_number_of_rotated_jobs(_) -> + Jobs = [ + continuous(1), + continuous_running(2), + continuous(3), + continuous_running(4) + ], + setup_jobs(Jobs), + reschedule(mock_state(2, 1)), + ?assertEqual([2, 3], jobs_stopped()). + +t_if_pending_less_than_running_start_all_pending(_) -> + Jobs = [ + continuous(1), + continuous_running(2), + continuous(3), + continuous_running(4), + continuous_running(5) + ], + setup_jobs(Jobs), + reschedule(mock_state(3)), + ?assertEqual([1, 2, 5], jobs_running()). + +t_running_less_than_pending_swap_all_running(_) -> + Jobs = [ + continuous(1), + continuous(2), + continuous(3), + continuous_running(4), + continuous_running(5) + ], + setup_jobs(Jobs), + reschedule(mock_state(2)), + ?assertEqual([3, 4, 5], jobs_stopped()). + +t_oneshot_dont_get_rotated(_) -> + setup_jobs([oneshot_running(1), continuous(2)]), + reschedule(mock_state(1)), + ?assertEqual([1], jobs_running()). + +t_rotate_continuous_only_if_mixed(_) -> + setup_jobs([continuous(1), oneshot_running(2), continuous_running(3)]), + reschedule(mock_state(2)), + ?assertEqual([1, 2], jobs_running()). + +t_oneshot_dont_get_starting_priority(_) -> + setup_jobs([continuous(1), oneshot(2), continuous_running(3)]), + reschedule(mock_state(1)), + ?assertEqual([1], jobs_running()). % This tested in other test cases, it is here to mainly make explicit a property % of one-shot replications -- they can starve other jobs if they "take control" % of all the available scheduler slots. -t_oneshot_will_hog_the_scheduler() -> - ?_test(begin - Jobs = [ - oneshot_running(1), - oneshot_running(2), - oneshot(3), - continuous(4) - ], - setup_jobs(Jobs), - reschedule(mock_state(2)), - ?assertEqual([1, 2], jobs_running()) - end). - -t_if_excess_is_trimmed_rotation_still_happens() -> - ?_test(begin - Jobs = [ - continuous(1), - continuous_running(2), - continuous_running(3) - ], - setup_jobs(Jobs), - reschedule(mock_state(1)), - ?assertEqual([1], jobs_running()) - end). - -t_if_transient_job_crashes_it_gets_removed() -> - ?_test(begin - Pid = mock_pid(), - Rep = continuous_rep(), - Job = #job{ - id = job1, - pid = Pid, - history = [added()], - rep = Rep#rep{db_name = null} - }, - setup_jobs([Job]), - ?assertEqual(1, ets:info(?MODULE, size)), - State = #state{max_history = 3, stats_pid = self()}, - {noreply, State} = handle_info( - {'EXIT', Pid, failed}, - State - ), - ?assertEqual(0, ets:info(?MODULE, size)) - end). - -t_if_permanent_job_crashes_it_stays_in_ets() -> - ?_test(begin - Pid = mock_pid(), - Rep = continuous_rep(), - Job = #job{ - id = job1, - pid = Pid, - history = [added()], - rep = Rep#rep{db_name = <<"db1">>} - }, - setup_jobs([Job]), - ?assertEqual(1, ets:info(?MODULE, size)), - State = #state{ - max_jobs = 1, - max_history = 3, - stats_pid = self() - }, - {noreply, State} = handle_info( - {'EXIT', Pid, failed}, - State - ), - ?assertEqual(1, ets:info(?MODULE, size)), - [Job1] = ets:lookup(?MODULE, job1), - [Latest | _] = Job1#job.history, - ?assertMatch({{crashed, failed}, _}, Latest) - end). - -t_existing_jobs() -> - ?_test(begin - Rep0 = continuous_rep(<<"s">>, <<"t">>), - Rep = Rep0#rep{id = job1, db_name = <<"db">>}, - setup_jobs([#job{id = Rep#rep.id, rep = Rep}]), - NewRep0 = continuous_rep(<<"s">>, <<"t">>), - NewRep = NewRep0#rep{id = Rep#rep.id, db_name = <<"db">>}, - ?assert(existing_replication(NewRep)), - ?assertNot(existing_replication(NewRep#rep{source = <<"s1">>})), - ?assertNot(existing_replication(NewRep#rep{target = <<"t1">>})), - ?assertNot(existing_replication(NewRep#rep{options = []})) - end). - -t_job_summary_running() -> - ?_test(begin - Rep = rep(<<"s">>, <<"t">>), - Job = #job{ - id = job1, - pid = mock_pid(), - history = [added()], - rep = Rep#rep{db_name = <<"db1">>} - }, - setup_jobs([Job]), - Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), - ?assertEqual(running, proplists:get_value(state, Summary)), - ?assertEqual(null, proplists:get_value(info, Summary)), - ?assertEqual(0, proplists:get_value(error_count, Summary)), - - Stats = [{source_seq, <<"1-abc">>}], - handle_cast({update_job_stats, job1, Stats}, mock_state(1)), - Summary1 = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), - ?assertEqual({Stats}, proplists:get_value(info, Summary1)) - end). - -t_job_summary_pending() -> - ?_test(begin - Job = #job{ - id = job1, - pid = undefined, - history = [stopped(20), started(10), added()], - rep = rep(<<"s">>, <<"t">>) - }, - setup_jobs([Job]), - Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), - ?assertEqual(pending, proplists:get_value(state, Summary)), - ?assertEqual(null, proplists:get_value(info, Summary)), - ?assertEqual(0, proplists:get_value(error_count, Summary)), - - Stats = [{doc_write_failures, 1}], - handle_cast({update_job_stats, job1, Stats}, mock_state(1)), - Summary1 = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), - ?assertEqual({Stats}, proplists:get_value(info, Summary1)) - end). - -t_job_summary_crashing_once() -> - ?_test(begin - Job = #job{ - id = job1, - history = [crashed(?DEFAULT_HEALTH_THRESHOLD_SEC + 1), started(0)], - rep = rep(<<"s">>, <<"t">>) - }, - setup_jobs([Job]), - Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), - ?assertEqual(crashing, proplists:get_value(state, Summary)), - Info = proplists:get_value(info, Summary), - ?assertEqual({[{<<"error">>, <<"some_reason">>}]}, Info), - ?assertEqual(0, proplists:get_value(error_count, Summary)) - end). - -t_job_summary_crashing_many_times() -> - ?_test(begin - Job = #job{ - id = job1, - history = [crashed(4), started(3), crashed(2), started(1)], - rep = rep(<<"s">>, <<"t">>) - }, - setup_jobs([Job]), - Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), - ?assertEqual(crashing, proplists:get_value(state, Summary)), - Info = proplists:get_value(info, Summary), - ?assertEqual({[{<<"error">>, <<"some_reason">>}]}, Info), - ?assertEqual(2, proplists:get_value(error_count, Summary)) - end). - -t_job_summary_proxy_fields() -> - ?_test(begin - Src = #httpdb{ - url = "https://s", - proxy_url = "http://u:p@sproxy:12" - }, - Tgt = #httpdb{ - url = "http://t", - proxy_url = "socks5://u:p@tproxy:34" - }, - Job = #job{ - id = job1, - history = [started(10), added()], - rep = rep(Src, Tgt) - }, - setup_jobs([Job]), - Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), - ?assertEqual( - <<"http://u:*****@sproxy:12">>, - proplists:get_value(source_proxy, Summary) - ), - ?assertEqual( - <<"socks5://u:*****@tproxy:34">>, - proplists:get_value(target_proxy, Summary) - ) - end). +t_oneshot_will_hog_the_scheduler(_) -> + Jobs = [ + oneshot_running(1), + oneshot_running(2), + oneshot(3), + continuous(4) + ], + setup_jobs(Jobs), + reschedule(mock_state(2)), + ?assertEqual([1, 2], jobs_running()). + +t_if_excess_is_trimmed_rotation_still_happens(_) -> + Jobs = [ + continuous(1), + continuous_running(2), + continuous_running(3) + ], + setup_jobs(Jobs), + reschedule(mock_state(1)). + +t_if_transient_job_crashes_it_gets_removed(_) -> + Pid = mock_pid(), + Rep = continuous_rep(), + Job = #job{ + id = job1, + pid = Pid, + history = [added()], + rep = Rep#rep{db_name = null} + }, + setup_jobs([Job]), + ?assertEqual(1, ets:info(?MODULE, size)), + State = #state{max_history = 3, stats_pid = self()}, + {noreply, State} = handle_info( + {'EXIT', Pid, failed}, + State + ), + ?assertEqual(0, ets:info(?MODULE, size)). + +t_if_permanent_job_crashes_it_stays_in_ets(_) -> + Pid = mock_pid(), + Rep = continuous_rep(), + Job = #job{ + id = job1, + pid = Pid, + history = [added()], + rep = Rep#rep{db_name = <<"db1">>} + }, + setup_jobs([Job]), + ?assertEqual(1, ets:info(?MODULE, size)), + State = #state{ + max_jobs = 1, + max_history = 3, + stats_pid = self() + }, + {noreply, State} = handle_info( + {'EXIT', Pid, failed}, + State + ), + ?assertEqual(1, ets:info(?MODULE, size)), + [Job1] = ets:lookup(?MODULE, job1), + [Latest | _] = Job1#job.history, + ?assertMatch({{crashed, failed}, _}, Latest). + +t_stop_all_stops_jobs(_) -> + Jobs = [ + oneshot_running(1), + oneshot_running(2), + oneshot(3), + continuous(4) + ], + setup_jobs(Jobs), + ?assertEqual(ok, stop_clear_all_jobs(?TERMINATE_SHUTDOWN_TIME)), + ?assertEqual([], jobs_running()), + ?assertEqual(0, ets:info(?MODULE, size)). + +t_existing_jobs(_) -> + Rep0 = continuous_rep(<<"s">>, <<"t">>), + Rep = Rep0#rep{id = job1, db_name = <<"db">>}, + setup_jobs([#job{id = Rep#rep.id, rep = Rep}]), + NewRep0 = continuous_rep(<<"s">>, <<"t">>), + NewRep = NewRep0#rep{id = Rep#rep.id, db_name = <<"db">>}, + ?assert(existing_replication(NewRep)), + ?assertNot(existing_replication(NewRep#rep{source = <<"s1">>})), + ?assertNot(existing_replication(NewRep#rep{target = <<"t1">>})), + ?assertNot(existing_replication(NewRep#rep{options = []})). + +t_job_summary_running(_) -> + Rep = rep(<<"s">>, <<"t">>), + Job = #job{ + id = job1, + pid = mock_pid(), + history = [added()], + rep = Rep#rep{db_name = <<"db1">>} + }, + setup_jobs([Job]), + Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), + ?assertEqual(running, proplists:get_value(state, Summary)), + ?assertEqual(null, proplists:get_value(info, Summary)), + ?assertEqual(0, proplists:get_value(error_count, Summary)), + + Stats = [{source_seq, <<"1-abc">>}], + handle_cast({update_job_stats, job1, Stats}, mock_state(1)), + Summary1 = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), + ?assertEqual({Stats}, proplists:get_value(info, Summary1)). + +t_job_summary_pending(_) -> + Job = #job{ + id = job1, + pid = undefined, + history = [stopped(20), started(10), added()], + rep = rep(<<"s">>, <<"t">>) + }, + setup_jobs([Job]), + Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), + ?assertEqual(pending, proplists:get_value(state, Summary)), + ?assertEqual(null, proplists:get_value(info, Summary)), + ?assertEqual(0, proplists:get_value(error_count, Summary)), + + Stats = [{doc_write_failures, 1}], + handle_cast({update_job_stats, job1, Stats}, mock_state(1)), + Summary1 = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), + ?assertEqual({Stats}, proplists:get_value(info, Summary1)). + +t_job_summary_crashing_once(_) -> + Job = #job{ + id = job1, + history = [crashed(?DEFAULT_HEALTH_THRESHOLD_SEC + 1), started(0)], + rep = rep(<<"s">>, <<"t">>) + }, + setup_jobs([Job]), + Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), + ?assertEqual(crashing, proplists:get_value(state, Summary)), + Info = proplists:get_value(info, Summary), + ?assertEqual({[{<<"error">>, <<"some_reason">>}]}, Info), + ?assertEqual(0, proplists:get_value(error_count, Summary)). + +t_job_summary_crashing_many_times(_) -> + Job = #job{ + id = job1, + history = [crashed(4), started(3), crashed(2), started(1)], + rep = rep(<<"s">>, <<"t">>) + }, + setup_jobs([Job]), + Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), + ?assertEqual(crashing, proplists:get_value(state, Summary)), + Info = proplists:get_value(info, Summary), + ?assertEqual({[{<<"error">>, <<"some_reason">>}]}, Info), + ?assertEqual(2, proplists:get_value(error_count, Summary)). + +t_job_summary_proxy_fields(_) -> + Src = #httpdb{ + url = "https://s", + proxy_url = "http://u:p@sproxy:12" + }, + Tgt = #httpdb{ + url = "http://t", + proxy_url = "socks5://u:p@tproxy:34" + }, + Job = #job{ + id = job1, + history = [started(10), added()], + rep = rep(Src, Tgt) + }, + setup_jobs([Job]), + Summary = job_summary(job1, ?DEFAULT_HEALTH_THRESHOLD_SEC), + ?assertEqual( + <<"http://u:*****@sproxy:12">>, + proplists:get_value(source_proxy, Summary) + ), + ?assertEqual( + <<"socks5://u:*****@tproxy:34">>, + proplists:get_value(target_proxy, Summary) + ). % Test helper functions diff --git a/src/couch_replicator/src/couch_replicator_scheduler_job.erl b/src/couch_replicator/src/couch_replicator_scheduler_job.erl index 544c5602a3..f82e362693 100644 --- a/src/couch_replicator/src/couch_replicator_scheduler_job.erl +++ b/src/couch_replicator/src/couch_replicator_scheduler_job.erl @@ -25,7 +25,7 @@ handle_call/3, handle_info/2, handle_cast/2, - format_status/2, + format_status/1, sum_stats/2, report_seq_done/3 ]). @@ -47,6 +47,7 @@ -define(LOWEST_SEQ, 0). -define(DEFAULT_CHECKPOINT_INTERVAL, 30000). -define(STARTUP_JITTER_DEFAULT, 5000). +-define(STOP_TIMEOUT_MSEC, 5000). -record(rep_state, { rep_details, @@ -104,13 +105,14 @@ start_link(#rep{id = Id = {BaseId, Ext}, source = Src, target = Tgt} = Rep) -> end. stop(Pid) when is_pid(Pid) -> - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), unlink(Pid), % In the rare case the job is already stopping as we try to stop it, it % won't return ok but exit the calling process, usually the scheduler, so % we guard against that. See: % www.erlang.org/doc/apps/stdlib/gen_server.html#stop/3 - catch gen_server:stop(Pid, shutdown, infinity), + catch gen_server:stop(Pid, shutdown, ?STOP_TIMEOUT_MSEC), + exit(Pid, kill), receive {'DOWN', Ref, _, _, Reason} -> Reason end, @@ -475,38 +477,46 @@ terminate_cleanup(#rep_state{rep_details = #rep{id = RepId}} = State) -> couch_replicator_api_wrap:db_close(State#rep_state.source), couch_replicator_api_wrap:db_close(State#rep_state.target). -format_status(_Opt, [_PDict, State]) -> - #rep_state{ - source = Source, - target = Target, - rep_details = RepDetails, - start_seq = StartSeq, - source_seq = SourceSeq, - committed_seq = CommitedSeq, - current_through_seq = ThroughSeq, - highest_seq_done = HighestSeqDone, - session_id = SessionId - } = state_strip_creds(State), - #rep{ - id = RepId, - options = Options, - doc_id = DocId, - db_name = DbName - } = RepDetails, - [ - {rep_id, RepId}, - {source, couch_replicator_api_wrap:db_uri(Source)}, - {target, couch_replicator_api_wrap:db_uri(Target)}, - {db_name, DbName}, - {doc_id, DocId}, - {options, Options}, - {session_id, SessionId}, - {start_seq, StartSeq}, - {source_seq, SourceSeq}, - {committed_seq, CommitedSeq}, - {current_through_seq, ThroughSeq}, - {highest_seq_done, HighestSeqDone} - ]. +format_status(Status) -> + maps:map( + fun + (state, State) -> + #rep_state{ + source = Source, + target = Target, + rep_details = RepDetails, + start_seq = StartSeq, + source_seq = SourceSeq, + committed_seq = CommitedSeq, + current_through_seq = ThroughSeq, + highest_seq_done = HighestSeqDone, + session_id = SessionId + } = state_strip_creds(State), + #rep{ + id = RepId, + options = Options, + doc_id = DocId, + db_name = DbName + } = RepDetails, + #{ + rep_id => RepId, + source => couch_replicator_api_wrap:db_uri(Source), + target => couch_replicator_api_wrap:db_uri(Target), + db_name => DbName, + doc_id => DocId, + options => Options, + session_id => SessionId, + start_seq => StartSeq, + source_seq => SourceSeq, + committed_seq => CommitedSeq, + current_through_seq => ThroughSeq, + highest_seq_done => HighestSeqDone + }; + (_, Value) -> + Value + end, + Status + ). sum_stats(Pid, Stats) when is_pid(Pid) -> gen_server:cast(Pid, {sum_stats, Stats}). @@ -520,7 +530,7 @@ startup_jitter() -> "startup_jitter", ?STARTUP_JITTER_DEFAULT ), - rand:uniform(erlang:max(1, Jitter)). + rand:uniform(max(1, Jitter)). headers_strip_creds([], Acc) -> lists:reverse(Acc); @@ -1228,29 +1238,31 @@ t_scheduler_job_format_status(_) -> doc_id = <<"mydoc">>, db_name = <<"mydb">> }, - State = #rep_state{ - rep_details = Rep, - source = Rep#rep.source, - target = Rep#rep.target, - session_id = <<"a">>, - start_seq = <<"1">>, - source_seq = <<"2">>, - committed_seq = <<"3">>, - current_through_seq = <<"4">>, - highest_seq_done = <<"5">> + Status = #{ + state => #rep_state{ + rep_details = Rep, + source = Rep#rep.source, + target = Rep#rep.target, + session_id = <<"a">>, + start_seq = <<"1">>, + source_seq = <<"2">>, + committed_seq = <<"3">>, + current_through_seq = <<"4">>, + highest_seq_done = <<"5">> + } }, - Format = format_status(opts_ignored, [pdict, State]), - ?assertEqual("http://h1/d1/", proplists:get_value(source, Format)), - ?assertEqual("http://h2/d2/", proplists:get_value(target, Format)), - ?assertEqual({"base", "+ext"}, proplists:get_value(rep_id, Format)), - ?assertEqual([{create_target, true}], proplists:get_value(options, Format)), - ?assertEqual(<<"mydoc">>, proplists:get_value(doc_id, Format)), - ?assertEqual(<<"mydb">>, proplists:get_value(db_name, Format)), - ?assertEqual(<<"a">>, proplists:get_value(session_id, Format)), - ?assertEqual(<<"1">>, proplists:get_value(start_seq, Format)), - ?assertEqual(<<"2">>, proplists:get_value(source_seq, Format)), - ?assertEqual(<<"3">>, proplists:get_value(committed_seq, Format)), - ?assertEqual(<<"4">>, proplists:get_value(current_through_seq, Format)), - ?assertEqual(<<"5">>, proplists:get_value(highest_seq_done, Format)). + #{state := State} = format_status(Status), + ?assertEqual("http://h1/d1/", maps:get(source, State)), + ?assertEqual("http://h2/d2/", maps:get(target, State)), + ?assertEqual({"base", "+ext"}, maps:get(rep_id, State)), + ?assertEqual([{create_target, true}], maps:get(options, State)), + ?assertEqual(<<"mydoc">>, maps:get(doc_id, State)), + ?assertEqual(<<"mydb">>, maps:get(db_name, State)), + ?assertEqual(<<"a">>, maps:get(session_id, State)), + ?assertEqual(<<"1">>, maps:get(start_seq, State)), + ?assertEqual(<<"2">>, maps:get(source_seq, State)), + ?assertEqual(<<"3">>, maps:get(committed_seq, State)), + ?assertEqual(<<"4">>, maps:get(current_through_seq, State)), + ?assertEqual(<<"5">>, maps:get(highest_seq_done, State)). -endif. diff --git a/src/couch_replicator/src/couch_replicator_utils.erl b/src/couch_replicator/src/couch_replicator_utils.erl index 9ce7866460..5e8187200b 100644 --- a/src/couch_replicator/src/couch_replicator_utils.erl +++ b/src/couch_replicator/src/couch_replicator_utils.erl @@ -30,7 +30,8 @@ normalize_basic_auth/1, seq_encode/1, valid_endpoint_protocols_log/1, - verify_ssl_certificates_log/1 + verify_ssl_certificates_log/1, + cacert_get/0 ]). -include_lib("ibrowse/include/ibrowse.hrl"). @@ -40,6 +41,10 @@ -include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl"). -include_lib("public_key/include/public_key.hrl"). +-define(CACERT_KEY, {?MODULE, cacert_timestamp_key}). +-define(CACERT_DEFAULT_TIMESTAMP, -(1 bsl 59)). +-define(CACERT_DEFAULT_INTERVAL_HOURS, 24). + -import(couch_util, [ get_value/2, get_value/3 @@ -76,7 +81,7 @@ get_json_value(Key, Props, Default) when is_atom(Key) -> Ref = make_ref(), case get_value(Key, Props, Ref) of Ref -> - get_value(?l2b(atom_to_list(Key)), Props, Default); + get_value(atom_to_binary(Key), Props, Default); Else -> Else end; @@ -84,7 +89,7 @@ get_json_value(Key, Props, Default) when is_binary(Key) -> Ref = make_ref(), case get_value(Key, Props, Ref) of Ref -> - get_value(list_to_atom(?b2l(Key)), Props, Default); + get_value(binary_to_atom(Key), Props, Default); Else -> Else end. @@ -402,6 +407,34 @@ rep_principal(#rep{user_ctx = #user_ctx{name = Name}}) when is_binary(Name) -> rep_principal(#rep{}) -> "by unknown principal". +cacert_get() -> + Now = erlang:monotonic_time(second), + Max = cacert_reload_interval_sec(), + TStamp = persistent_term:get(?CACERT_KEY, ?CACERT_DEFAULT_TIMESTAMP), + cacert_load(TStamp, Now, Max), + public_key:cacerts_get(). + +cacert_load(TStamp, Now, Max) when (Now - TStamp) > Max -> + public_key:cacerts_clear(), + case public_key:cacerts_load() of + ok -> + Count = length(public_key:cacerts_get()), + InfoMsg = "~p : loaded ~p os ca certificates", + couch_log:info(InfoMsg, [?MODULE, Count]); + {error, Reason} -> + ErrMsg = "~p : error loading os ca certificates: ~p", + couch_log:error(ErrMsg, [?MODULE, Reason]) + end, + persistent_term:put(?CACERT_KEY, Now), + loaded; +cacert_load(_TStamp, _Now, _Max) -> + not_loaded. + +cacert_reload_interval_sec() -> + Default = ?CACERT_DEFAULT_INTERVAL_HOURS, + Hrs = config:get_integer("replicator", "cacert_reload_interval_hours", Default), + Hrs * 3600. + -ifdef(TEST). -include_lib("couch/include/couch_eunit.hrl"). @@ -778,4 +811,19 @@ t_allow_canceling_transient_jobs(_) -> ?assertEqual(ok, valid_endpoint_protocols_log(#rep{})), ?assertEqual(0, meck:num_calls(couch_log, warning, 2)). +cacert_test() -> + Old = ?CACERT_DEFAULT_TIMESTAMP, + Now = erlang:monotonic_time(second), + Max = 0, + ?assertEqual(loaded, cacert_load(Old, Now, Max)), + ?assertEqual(not_loaded, cacert_load(Now, Now, Max)), + try cacert_get() of + CACerts -> + ?assert(is_list(CACerts)) + catch + error:_Err -> + % This is ok, some environments may not have OS certs + ?assert(true) + end. + -endif. diff --git a/src/couch_replicator/src/couch_replicator_worker.erl b/src/couch_replicator/src/couch_replicator_worker.erl index 078e6e7e0f..3a6edc0b85 100644 --- a/src/couch_replicator/src/couch_replicator_worker.erl +++ b/src/couch_replicator/src/couch_replicator_worker.erl @@ -19,7 +19,7 @@ % gen_server callbacks -export([init/1]). -export([handle_call/3, handle_cast/2, handle_info/2]). --export([format_status/2]). +-export([format_status/1]). -include_lib("couch/include/couch_db.hrl"). -include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl"). @@ -242,25 +242,33 @@ handle_info({'EXIT', _Pid, {doc_write_failed, _} = Err}, State) -> handle_info({'EXIT', Pid, Reason}, State) -> {stop, {process_died, Pid, Reason}, State}. -format_status(_Opt, [_PDict, State]) -> - #state{ - cp = MainJobPid, - loop = LoopPid, - source = Source, - target = Target, - readers = Readers, - pending_fetch = PendingFetch, - batch = #batch{size = BatchSize} - } = State, - [ - {main_pid, MainJobPid}, - {loop, LoopPid}, - {source, couch_replicator_api_wrap:db_uri(Source)}, - {target, couch_replicator_api_wrap:db_uri(Target)}, - {num_readers, length(Readers)}, - {pending_fetch, PendingFetch}, - {batch_size, BatchSize} - ]. +format_status(Status) -> + maps:map( + fun + (state, State) -> + #state{ + cp = MainJobPid, + loop = LoopPid, + source = Source, + target = Target, + readers = Readers, + pending_fetch = PendingFetch, + batch = #batch{size = BatchSize} + } = State, + #{ + main_pid => MainJobPid, + loop => LoopPid, + source => couch_replicator_api_wrap:db_uri(Source), + target => couch_replicator_api_wrap:db_uri(Target), + num_readers => length(Readers), + pending_fetch => PendingFetch, + batch_size => BatchSize + }; + (_, Value) -> + Value + end, + Status + ). sum_stats(Pid, Stats) when is_pid(Pid) -> ok = gen_server:cast(Pid, {sum_stats, Stats}). @@ -733,23 +741,25 @@ maybe_report_stats(#state{} = State) -> -include_lib("eunit/include/eunit.hrl"). replication_worker_format_status_test() -> - State = #state{ - cp = self(), - loop = self(), - source = #httpdb{url = "http://u:p@h/d1"}, - target = #httpdb{url = "http://u:p@h/d2"}, - readers = [r1, r2, r3], - pending_fetch = nil, - batch = #batch{size = 5} + Status = #{ + state => #state{ + cp = self(), + loop = self(), + source = #httpdb{url = "http://u:p@h/d1"}, + target = #httpdb{url = "http://u:p@h/d2"}, + readers = [r1, r2, r3], + pending_fetch = nil, + batch = #batch{size = 5} + } }, - Format = format_status(opts_ignored, [pdict, State]), - ?assertEqual(self(), proplists:get_value(main_pid, Format)), - ?assertEqual(self(), proplists:get_value(loop, Format)), - ?assertEqual("http://u:*****@h/d1", proplists:get_value(source, Format)), - ?assertEqual("http://u:*****@h/d2", proplists:get_value(target, Format)), - ?assertEqual(3, proplists:get_value(num_readers, Format)), - ?assertEqual(nil, proplists:get_value(pending_fetch, Format)), - ?assertEqual(5, proplists:get_value(batch_size, Format)). + #{state := State} = format_status(Status), + ?assertEqual(self(), maps:get(main_pid, State)), + ?assertEqual(self(), maps:get(loop, State)), + ?assertEqual("http://u:*****@h/d1", maps:get(source, State)), + ?assertEqual("http://u:*****@h/d2", maps:get(target, State)), + ?assertEqual(3, maps:get(num_readers, State)), + ?assertEqual(nil, maps:get(pending_fetch, State)), + ?assertEqual(5, maps:get(batch_size, State)). bulk_get_attempt_test() -> Now = erlang:monotonic_time(second), diff --git a/src/couch_replicator/src/json_stream_parse.erl b/src/couch_replicator/src/json_stream_parse.erl index a76c1dffff..8b0c34fa22 100644 --- a/src/couch_replicator/src/json_stream_parse.erl +++ b/src/couch_replicator/src/json_stream_parse.erl @@ -300,7 +300,7 @@ toke_string(DF, <<$\\, $t, Rest/binary>>, Acc) -> toke_string(DF, Rest, [$\t | Acc]); toke_string(DF, <<$\\, $u, Rest/binary>>, Acc) -> {<>, DF2} = must_df(DF, 4, Rest, missing_hex), - UTFChar = erlang:list_to_integer([A, B, C, D], 16), + UTFChar = list_to_integer([A, B, C, D], 16), if UTFChar == 16#FFFF orelse UTFChar == 16#FFFE -> err(invalid_utf_char); diff --git a/src/couch_replicator/test/eunit/couch_replicator_compact_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_compact_tests.erl index df8074f1fd..d3e883f846 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_compact_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_compact_tests.erl @@ -170,7 +170,7 @@ wait_target_in_sync(Source, Target) -> wait_target_in_sync_loop(SourceDocCount, Target, 300). wait_target_in_sync_loop(_DocCount, _TargetName, 0) -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -202,7 +202,7 @@ compare_databases(Source, Target) -> {ok, DocT} -> DocT; Error -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -235,14 +235,14 @@ compact_db(Type, Db0) -> Name = couch_db:name(Db0), {ok, Db} = couch_db:open_int(Name, []), {ok, CompactPid} = couch_db:start_compact(Db), - MonRef = erlang:monitor(process, CompactPid), + MonRef = monitor(process, CompactPid), receive {'DOWN', MonRef, process, CompactPid, normal} -> ok; {'DOWN', MonRef, process, CompactPid, noproc} -> ok; {'DOWN', MonRef, process, CompactPid, Reason} -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -258,7 +258,7 @@ compact_db(Type, Db0) -> ]} ) after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -282,7 +282,7 @@ wait_for_compaction(Type, Db) -> {error, noproc} -> ok; {error, Reason} -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -330,7 +330,7 @@ pause_writer(Pid) -> {paused, Ref} -> ok after ?TIMEOUT_WRITER -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -346,7 +346,7 @@ resume_writer(Pid) -> {ok, Ref} -> ok after ?TIMEOUT_WRITER -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -362,7 +362,7 @@ get_writer_num_docs_written(Pid) -> {count, Ref, Count} -> Count after ?TIMEOUT_WRITER -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -378,12 +378,12 @@ stop_writer(Pid) -> Pid ! {stop, Ref}, receive {stopped, Ref, DocsWritten} -> - MonRef = erlang:monitor(process, Pid), + MonRef = monitor(process, Pid), receive {'DOWN', MonRef, process, Pid, _Reason} -> DocsWritten after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -392,7 +392,7 @@ stop_writer(Pid) -> ) end after ?TIMEOUT_WRITER -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -409,7 +409,7 @@ writer_loop(Db0, Parent, Counter) -> fun(I) -> couch_doc:from_json_obj( {[ - {<<"_id">>, ?l2b(integer_to_list(Counter + I))}, + {<<"_id">>, integer_to_binary(Counter + I)}, {<<"value">>, Counter + I}, {<<"_attachments">>, {[ diff --git a/src/couch_replicator/test/eunit/couch_replicator_connection_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_connection_tests.erl index aa75bd746f..9e39122548 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_connection_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_connection_tests.erl @@ -187,7 +187,7 @@ user_pass() -> {User, Pass, B64Auth}. worker_internals(Pid) -> - Dict = io_lib:format("~p", [erlang:process_info(Pid, dictionary)]), + Dict = io_lib:format("~p", [process_info(Pid, dictionary)]), State = io_lib:format("~p", [sys:get_state(Pid)]), lists:flatten([Dict, State]). diff --git a/src/couch_replicator/test/eunit/couch_replicator_error_reporting_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_error_reporting_tests.erl index 2d6079a28e..4988bb41df 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_error_reporting_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_error_reporting_tests.erl @@ -454,7 +454,7 @@ wait_target_in_sync(Source, Target) -> wait_target_in_sync_loop(SourceDocCount, Target, 300). wait_target_in_sync_loop(_DocCount, _TargetName, 0) -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch_replicator/test/eunit/couch_replicator_httpc_pool_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_httpc_pool_tests.erl index 512d020370..c8e6d0af74 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_httpc_pool_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_httpc_pool_tests.erl @@ -120,7 +120,7 @@ get_client_worker({Pid, Ref}, ClientName) -> {worker, Ref, Worker} -> Worker after ?TIMEOUT -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch_replicator/test/eunit/couch_replicator_rate_limiter_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_rate_limiter_tests.erl index acb931b9bf..03c1c1a5f7 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_rate_limiter_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_rate_limiter_tests.erl @@ -57,7 +57,7 @@ setup() -> Pid. teardown(Pid) -> - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), unlink(Pid), exit(Pid, kill), receive diff --git a/src/couch_replicator/test/eunit/couch_replicator_retain_stats_between_job_runs.erl b/src/couch_replicator/test/eunit/couch_replicator_retain_stats_between_job_runs.erl index f413e5cf4e..47fbaa7c6f 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_retain_stats_between_job_runs.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_retain_stats_between_job_runs.erl @@ -80,13 +80,13 @@ t_stats_retained_on_job_removal({_Ctx, {Source, Target}}) -> couch_replicator_scheduler:remove_job(RepId). stop_job(RepPid) -> - Ref = erlang:monitor(process, RepPid), + Ref = monitor(process, RepPid), gen_server:cast(couch_replicator_scheduler, {set_max_jobs, 0}), couch_replicator_scheduler:reschedule(), receive {'DOWN', Ref, _, _, _} -> ok after ?TIMEOUT -> - erlang:error(timeout) + error(timeout) end. start_job() -> @@ -187,7 +187,7 @@ wait_target_in_sync(DocCount, Target) when is_integer(DocCount) -> wait_target_in_sync_loop(DocCount, Target, 300). wait_target_in_sync_loop(_DocCount, _TargetName, 0) -> - erlang:error( + error( {assertion_failed, [ {module, ?MODULE}, {line, ?LINE}, diff --git a/src/couch_replicator/test/eunit/couch_replicator_small_max_request_size_target.erl b/src/couch_replicator/test/eunit/couch_replicator_small_max_request_size_target.erl index c7e89e8be6..88e9e8801c 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_small_max_request_size_target.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_small_max_request_size_target.erl @@ -63,7 +63,7 @@ binary_chunk(Size) when is_integer(Size), Size > 0 -> add_docs(DbName, DocCount, DocSize, AttSize) -> [ begin - DocId = iolist_to_binary(["doc", integer_to_list(Id)]), + DocId = <<"doc", (integer_to_binary(Id))/binary>>, add_doc(DbName, DocId, DocSize, AttSize) end || Id <- lists:seq(1, DocCount) diff --git a/src/couch_replicator/test/eunit/couch_replicator_test_helper.erl b/src/couch_replicator/test/eunit/couch_replicator_test_helper.erl index 843af79192..5f2cfa25f2 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_test_helper.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_test_helper.erl @@ -181,7 +181,7 @@ replicate({[_ | _]} = RepObject) -> ok = couch_replicator_scheduler:add_job(Rep), couch_replicator_scheduler:reschedule(), Pid = get_pid(Rep#rep.id), - MonRef = erlang:monitor(process, Pid), + MonRef = monitor(process, Pid), receive {'DOWN', MonRef, process, Pid, _} -> ok diff --git a/src/couch_scanner/src/couch_scanner_plugin.erl b/src/couch_scanner/src/couch_scanner_plugin.erl index 04f394c128..84bbf64964 100644 --- a/src/couch_scanner/src/couch_scanner_plugin.erl +++ b/src/couch_scanner/src/couch_scanner_plugin.erl @@ -115,15 +115,25 @@ -callback shards(St :: term(), [#shard{}]) -> {[#shard{}], St1 :: term()}. -% Optional +% Optional. Called right after a shard file is opened so it gets a Db handle. +% Should return the change feed start sequence and a list of options along with any changes +% in a private context. The change feed start sequence should normally be 0 and the list +% of option can be []. The list of options will be passed directly to couch_db:fold_changes, +% so any {dir, Dir}, {end_key, EndSeq} could work there. +% -callback db_opened(St :: term(), Db :: term()) -> - {ok, St :: term()}. + {ChangesSeq :: non_neg_integer(), ChangesOpts :: [term()], St1 :: term()}. -% Optional. If doc is not defined, then ddoc_id default action is {skip, St}. -% If it is defined, the default action is {ok, St}. +% Optional. If doc and doc_fdi are not defined, then doc_id default +% action is {skip, St}. If it is defined, the default action is {ok, St}. -callback doc_id(St :: term(), DocId :: binary(), Db :: term()) -> {ok | skip | stop, St1 :: term()}. +% Optional. If doc is not defined, then doc_fdi default action is {stop, St}. +% If it is defined, the default action is {ok, St}. +-callback doc_fdi(St :: term(), FDI :: #full_doc_info{}, Db :: term()) -> + {ok | stop, St1 :: term()}. + % Optional. -callback doc(St :: term(), Db :: term(), #doc{}) -> {ok | stop, St1 :: term()}. @@ -139,6 +149,7 @@ shards/2, db_opened/2, doc_id/3, + doc_fdi/3, doc/3, db_closing/2 ]). @@ -153,12 +164,14 @@ {shards, 2, fun default_shards/3}, {db_opened, 2, fun default_db_opened/3}, {doc_id, 3, fun default_doc_id/3}, + {doc_fdi, 3, fun default_doc_fdi/3}, {doc, 3, fun default_doc/3}, {db_closing, 2, fun default_db_closing/3} ]). -define(CHECKPOINT_INTERVAL_SEC, 10). -define(STOP_TIMEOUT_SEC, 5). +-define(DDOC_BATCH_SIZE, 100). -record(st, { id, @@ -171,6 +184,8 @@ cursor, shards_db, db, + changes_seq = 0, + changes_opts = [], checkpoint_sec = 0, start_sec = 0, skip_dbs, @@ -183,7 +198,7 @@ spawn_link(Id) -> stop(Pid) when is_pid(Pid) -> unlink(Pid), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), Pid ! stop, receive {'DOWN', Ref, _, _, _} -> ok @@ -231,7 +246,7 @@ init_from_checkpoint(#st{} = St) -> <<"start_sec">> := StartSec } -> Now = tsec(), - PSt = resume_callback(Cbks, SId, EJsonPSt), + PSt = resume_callback(Mod, Cbks, SId, EJsonPSt), St#st{ pst = PSt, cursor = Cur, @@ -285,6 +300,9 @@ scan_dbs(#st{cursor = Cursor} = St) -> couch_db:close(Db) end. +scan_dbs_fold(#full_doc_info{id = <>}, Acc) -> + % In case user added a design doc in the dbs database + {ok, Acc}; scan_dbs_fold(#full_doc_info{} = FDI, #st{shards_db = Db} = Acc) -> Acc1 = Acc#st{cursor = FDI#full_doc_info.id}, Acc2 = maybe_checkpoint(Acc1), @@ -300,9 +318,7 @@ scan_dbs_fold(#full_doc_info{} = FDI, #st{shards_db = Db} = Acc) -> {ok, Acc2} end. -scan_db([], #st{} = St) -> - {ok, St}; -scan_db([_ | _] = Shards, #st{} = St) -> +scan_db(Shards, #st{} = St) -> #st{dbname = DbName, callbacks = Cbks, pst = PSt, skip_dbs = Skip} = St, #{db := DbCbk} = Cbks, case match_skip_pat(DbName, Skip) of @@ -312,7 +328,7 @@ scan_db([_ | _] = Shards, #st{} = St) -> case Go of ok -> St2 = rate_limit(St1, db), - St3 = fold_ddocs(fun scan_ddocs_fold/2, St2), + St3 = scan_ddocs(St2), {Shards1, St4} = shards_callback(St3, Shards), St5 = scan_shards(Shards1, St4), {ok, St5}; @@ -325,16 +341,6 @@ scan_db([_ | _] = Shards, #st{} = St) -> {ok, St} end. -scan_ddocs_fold({meta, _}, #st{} = Acc) -> - {ok, Acc}; -scan_ddocs_fold({row, RowProps}, #st{} = Acc) -> - DDoc = couch_util:get_value(doc, RowProps), - scan_ddoc(ejson_to_doc(DDoc), Acc); -scan_ddocs_fold(complete, #st{} = Acc) -> - {ok, Acc}; -scan_ddocs_fold({error, Error}, _Acc) -> - exit({shutdown, {scan_ddocs_fold, Error}}). - scan_shards([], #st{} = St) -> St; scan_shards([#shard{} = Shard | Rest], #st{} = St) -> @@ -363,9 +369,10 @@ scan_docs(#st{} = St, #shard{name = ShardDbName}) -> try St2 = St1#st{db = Db}, St3 = db_opened_callback(St2), - {ok, St4} = couch_db:fold_docs(Db, fun scan_docs_fold/2, St3, []), + #st{changes_seq = Seq, changes_opts = Opts} = St3, + {ok, St4} = couch_db:fold_changes(Db, Seq, fun scan_docs_fold/2, St3, Opts), St5 = db_closing_callback(St4), - erlang:garbage_collect(), + garbage_collect(), St5#st{db = undefined} after couch_db:close(Db) @@ -382,7 +389,7 @@ scan_docs_fold(#full_doc_info{id = Id} = FDI, #st{} = St) -> {Go, PSt1} = DocIdCbk(PSt, Id, Db), St1 = St#st{pst = PSt1}, case Go of - ok -> scan_doc(FDI, St1); + ok -> scan_fdi(FDI, St1); skip -> {ok, St1}; stop -> {stop, St1} end; @@ -390,6 +397,16 @@ scan_docs_fold(#full_doc_info{id = Id} = FDI, #st{} = St) -> {ok, St} end. +scan_fdi(#full_doc_info{} = FDI, #st{} = St) -> + #st{db = Db, callbacks = Cbks, pst = PSt} = St, + #{doc_fdi := FDICbk} = Cbks, + {Go, PSt1} = FDICbk(PSt, FDI, Db), + St1 = St#st{pst = PSt1}, + case Go of + ok -> scan_doc(FDI, St1); + stop -> {stop, St1} + end. + scan_doc(#full_doc_info{} = FDI, #st{} = St) -> #st{db = Db, callbacks = Cbks, pst = PSt} = St, St1 = rate_limit(St, doc), @@ -410,7 +427,7 @@ maybe_checkpoint(#st{checkpoint_sec = LastCheckpointTSec} = St) -> stop -> exit({shutdown, stop}) after 0 -> ok end, - erlang:garbage_collect(), + garbage_collect(), case tsec() - LastCheckpointTSec > ?CHECKPOINT_INTERVAL_SEC of true -> checkpoint(St); false -> St @@ -486,31 +503,44 @@ start_callback(Mod, Cbks, Now, ScanId, LastStartSec, #{} = EJson) when TSec when is_integer(TSec), TSec =< Now -> #{start := StartCbk} = Cbks, case StartCbk(ScanId, EJson) of - {ok, PSt} -> PSt; - skip -> exit_resched(infinity); - reset -> exit_resched(reset) + {ok, PSt} -> + PSt; + skip -> + % If plugin skipped start, count this as an attempt and + % reschedule to possibly retry in the future. + SkipReschedTSec = schedule_time(Mod, Now, Now), + exit_resched(SkipReschedTSec); + reset -> + exit_resched(reset) end; TSec when is_integer(TSec), TSec > Now -> exit_resched(TSec) end. -resume_callback(#{} = Cbks, SId, #{} = EJsonPSt) when is_binary(SId) -> +resume_callback(Mod, #{} = Cbks, SId, #{} = EJsonPSt) when is_binary(SId) -> #{resume := ResumeCbk} = Cbks, case ResumeCbk(SId, EJsonPSt) of - {ok, PSt} -> PSt; - skip -> exit_resched(infinity); - reset -> exit_resched(reset) + {ok, PSt} -> + PSt; + skip -> + % If plugin skipped resume, count this as an attempt and + % reschedule to possibly retry in the future + Now = tsec(), + SkipReschedTSec = schedule_time(Mod, Now, Now), + exit_resched(SkipReschedTSec); + reset -> + exit_resched(reset) end. db_opened_callback(#st{pst = PSt, callbacks = Cbks, db = Db} = St) -> #{db_opened := DbOpenedCbk} = Cbks, - {ok, PSt1} = DbOpenedCbk(PSt, Db), - St#st{pst = PSt1}. + {Seq, Opts, PSt1} = DbOpenedCbk(PSt, Db), + St#st{pst = PSt1, changes_seq = Seq, changes_opts = Opts}. db_closing_callback(#st{pst = PSt, callbacks = Cbks, db = Db} = St) -> #{db_closing := DbClosingCbk} = Cbks, {ok, PSt1} = DbClosingCbk(PSt, Db), - St#st{pst = PSt1}. + St#st{pst = PSt1, changes_seq = 0, changes_opts = []}. shards_callback(#st{pst = PSt, callbacks = Cbks} = St, Shards) -> #{shards := ShardsCbk} = Cbks, @@ -575,6 +605,7 @@ default_shards(Mod, _F, _A) when is_atom(Mod) -> case is_exported(Mod, db_opened, 2) orelse is_exported(Mod, doc_id, 3) orelse + is_exported(Mod, doc_fdi, 3) orelse is_exported(Mod, doc, 3) orelse is_exported(Mod, db_closing, 2) of @@ -583,14 +614,20 @@ default_shards(Mod, _F, _A) when is_atom(Mod) -> end. default_db_opened(Mod, _F, _A) when is_atom(Mod) -> - fun(St, _Db) -> {ok, St} end. + fun(St, _Db) -> {0, [], St} end. default_doc_id(Mod, _F, _A) when is_atom(Mod) -> - case is_exported(Mod, doc, 3) of + case is_exported(Mod, doc, 3) orelse is_exported(Mod, doc_fdi, 3) of true -> fun(St, _DocId, _Db) -> {ok, St} end; false -> fun(St, _DocId, _Db) -> {skip, St} end end. +default_doc_fdi(Mod, _F, _A) when is_atom(Mod) -> + case is_exported(Mod, doc, 3) of + true -> fun(St, _FDI, _Db) -> {ok, St} end; + false -> fun(St, _FDI, _Db) -> {stop, St} end + end. + default_doc(Mod, _F, _A) when is_atom(Mod) -> fun(St, _Db, _Doc) -> {ok, St} end. @@ -622,27 +659,86 @@ shards_by_range(Shards) -> Dict = lists:foldl(Fun, orddict:new(), Shards), orddict:to_list(Dict). -% Design doc fetching helper +scan_ddocs(#st{mod = Mod} = St) -> + case is_exported(Mod, ddoc, 3) of + true -> + try + fold_ddocs_batched(St, <>) + catch + error:database_does_not_exist -> + St + end; + false -> + % If the plugin doesn't export the ddoc callback, don't bother calling + % fabric:all_docs, as it's expensive + St + end. -fold_ddocs(Fun, #st{dbname = DbName} = Acc) -> +fold_ddocs_batched(#st{dbname = DbName} = St, <<_/binary>> = StartKey) -> QArgs = #mrargs{ include_docs = true, - extra = [{namespace, <<"_design">>}] + start_key = StartKey, + extra = [{namespace, <>}], + % Need limit > 1 for the algorithm below to work + limit = max(2, cfg_ddoc_batch_size()) }, - try - {ok, Acc1} = fabric:all_docs(DbName, [?ADMIN_CTX], Fun, Acc, QArgs), - Acc1 - catch - error:database_does_not_exist -> - Acc + Cbk = + fun + ({meta, _}, {Cnt, Id, DDocs}) -> + {ok, {Cnt, Id, DDocs}}; + ({row, Props}, {Cnt, _Id, DDocs}) -> + EJson = couch_util:get_value(doc, Props), + DDoc = #doc{id = Id} = ejson_to_doc(EJson), + case Id =:= StartKey of + true -> + % We get there if we're continuing batched iteration so + % we skip this ddoc as we already processed it. In the + % first batch StartKey will be <<"_design/">> and + % that's an invalid document ID so will never match. + {ok, {Cnt + 1, Id, DDocs}}; + false -> + {ok, {Cnt + 1, Id, [DDoc | DDocs]}} + end; + (complete, {Cnt, Id, DDocs}) -> + {ok, {Cnt, Id, lists:reverse(DDocs)}}; + ({error, Error}, {_Cnt, _Id, _DDocs}) -> + exit({shutdown, {scan_ddocs_fold, Error}}) + end, + Acc0 = {0, StartKey, []}, + {ok, {Cnt, LastId, DDocs}} = fabric:all_docs(DbName, [?ADMIN_CTX], Cbk, Acc0, QArgs), + case scan_ddoc_batch(DDocs, {ok, St}) of + {ok, #st{} = St1} -> + if + is_integer(Cnt), Cnt < QArgs#mrargs.limit -> + % We got less than we asked for so we're done + St1; + Cnt == QArgs#mrargs.limit -> + % We got all the docs we asked for, there are probably more docs + % so we recurse and fetch the next batch. + fold_ddocs_batched(St1, LastId) + end; + {stop, #st{} = St1} -> + % Plugin wanted to stop scanning ddocs, so we stop + St1 end. +% Call plugin ddocs callback. These may take an arbitrarily long time to +% process. +scan_ddoc_batch(_, {stop, #st{} = St}) -> + {stop, St}; +scan_ddoc_batch([], {ok, #st{} = St}) -> + {ok, St}; +scan_ddoc_batch([#doc{} = DDoc | Rest], {ok, #st{} = St}) -> + scan_ddoc_batch(Rest, scan_ddoc(DDoc, St)). + % Simple ejson to #doc{} function to avoid all the extra validation in from_json_obj/1. % We just got these docs from the cluster, they are already saved on disk. ejson_to_doc({[_ | _] = Props}) -> {value, {_, DocId}, Props1} = lists:keytake(<<"_id">>, 1, Props), - Props2 = [{K, V} || {K, V} <- Props1, K =:= <<>> orelse binary:first(K) =/= $_], - #doc{id = DocId, body = {Props2}}. + {value, {_, Rev}, Props2} = lists:keytake(<<"_rev">>, 1, Props1), + {Pos, RevId} = couch_doc:parse_rev(Rev), + Props3 = [{K, V} || {K, V} <- Props2, K =:= <<>> orelse binary:first(K) =/= $_], + #doc{id = DocId, revs = {Pos, [RevId]}, body = {Props3}}. % Skip patterns @@ -667,6 +763,9 @@ cfg(Mod, Key, Default) when is_list(Key) -> Section = atom_to_list(Mod), config:get(Section, Key, Default). +cfg_ddoc_batch_size() -> + config:get_integer("couch_scanner", "ddoc_batch_size", ?DDOC_BATCH_SIZE). + schedule_time(Mod, LastSec, NowSec) -> After = cfg(Mod, "after", "restart"), Repeat = cfg(Mod, "repeat", "restart"), diff --git a/src/couch_scanner/src/couch_scanner_plugin_conflict_finder.erl b/src/couch_scanner/src/couch_scanner_plugin_conflict_finder.erl index 5f8f175271..4d57cbd91d 100644 --- a/src/couch_scanner/src/couch_scanner_plugin_conflict_finder.erl +++ b/src/couch_scanner/src/couch_scanner_plugin_conflict_finder.erl @@ -19,7 +19,7 @@ complete/1, checkpoint/1, db/2, - doc_id/3 + doc_fdi/3 ]). -include_lib("couch_scanner/include/couch_scanner_plugin.hrl"). @@ -76,12 +76,10 @@ checkpoint(#st{sid = SId, opts = Opts}) -> db(#st{} = St, _DbName) -> {ok, St}. -doc_id(#st{} = St, <>, _Db) -> - {skip, St}; -doc_id(#st{} = St, DocId, Db) -> - {ok, #doc_info{revs = Revs}} = couch_db:get_doc_info(Db, DocId), +doc_fdi(#st{} = St, #full_doc_info{} = FDI, Db) -> + #doc_info{revs = Revs} = couch_doc:to_doc_info(FDI), DbName = mem3:dbname(couch_db:name(Db)), - {ok, check(St, DbName, DocId, Revs)}. + {ok, check(St, DbName, FDI#full_doc_info.id, Revs)}. % Private diff --git a/src/couch_scanner/src/couch_scanner_plugin_find.erl b/src/couch_scanner/src/couch_scanner_plugin_find.erl index af5c8a5503..12b1e22b5a 100644 --- a/src/couch_scanner/src/couch_scanner_plugin_find.erl +++ b/src/couch_scanner/src/couch_scanner_plugin_find.erl @@ -23,7 +23,6 @@ complete/1, checkpoint/1, db/2, - ddoc/3, shards/2, db_opened/2, doc_id/3, @@ -77,11 +76,6 @@ db(#st{} = St, DbName) -> report_match(DbName, Pats, Meta), {ok, St}. -ddoc(#st{} = St, _DbName, #doc{} = _DDoc) -> - % We'll check doc bodies during the shard scan - % so no need to keep inspecting ddocs - {stop, St}. - shards(#st{sid = SId} = St, Shards) -> case debug() of true -> ?DEBUG(" ~p shards", [length(Shards)], #{sid => SId}); @@ -94,7 +88,9 @@ db_opened(#st{sid = SId} = St, Db) -> true -> ?DEBUG("", [], #{sid => SId, db => Db}); false -> ok end, - {ok, St}. + % Search backwards with the idea that we may be looking for some recent + % changes we just made to the database. + {couch_db:get_update_seq(Db), [{dir, rev}], St}. doc_id(#st{} = St, DocId, Db) -> #st{sid = SId, compiled_regexes = Pats} = St, diff --git a/src/couch_scanner/src/couch_scanner_rate_limiter.erl b/src/couch_scanner/src/couch_scanner_rate_limiter.erl index 2717be69e4..605445c19a 100644 --- a/src/couch_scanner/src/couch_scanner_rate_limiter.erl +++ b/src/couch_scanner/src/couch_scanner_rate_limiter.erl @@ -21,6 +21,24 @@ % % [1] https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease % +% Example of usage: +% +% initialize: +% Limiter = couch_scanner_rate_limiter:get(), +% +% use: +% bulk_docs(#{docs => [doc1, doc2, doc3]}), +% {Wait, Limiter1} = couch_scanner_rate_limiter:update(Limiter, doc_write, 3), +% timer:sleep(Wait) +% or +% receive .... after Wait -> ... end +% +% The Type can be: +% * db : rate of clustered db opens +% * shard: rate of shard files opened +% * doc : rate of document reads +% * doc_write : rate of document writes (or other per document updates, could be purges, too) +% -module(couch_scanner_rate_limiter). @@ -29,7 +47,8 @@ -export([ start_link/0, get/0, - update/2 + update/2, + update/3 ]). % gen_server callbacks @@ -62,16 +81,17 @@ -define(DB_RATE_DEFAULT, 25). -define(SHARD_RATE_DEFAULT, 50). -define(DOC_RATE_DEFAULT, 1000). +-define(DOC_WRITE_RATE_DEFAULT, 500). % Atomic ref indices. They start at 1. --define(INDICES, #{db => 1, shard => 2, doc => 3}). +-define(INDICES, #{db => 1, shard => 2, doc => 3, doc_write => 4}). % Record maintained by the clients. Each client will have one of these handles. % With each update/2 call they will update their own backoff values. % -record(client_st, { ref, - % db|shard|doc => {Backoff, UpdateTStamp} + % db|shard|doc|doc_write => {Backoff, UpdateTStamp} backoffs = #{} }). @@ -83,13 +103,17 @@ get() -> Ref = gen_server:call(?MODULE, get, infinity), NowMSec = erlang:monotonic_time(millisecond), - Backoffs = maps:from_keys([db, shard, doc], {?INIT_BACKOFF, NowMSec}), + Backoffs = maps:from_keys([db, shard, doc, doc_write], {?INIT_BACKOFF, NowMSec}), #client_st{ref = Ref, backoffs = Backoffs}. -update(#client_st{ref = Ref, backoffs = Backoffs} = St, Type) when - Type =:= db orelse Type =:= shard orelse Type =:= doc +update(St, Type) -> + update(St, Type, 1). + +update(#client_st{ref = Ref, backoffs = Backoffs} = St, Type, Count) when + (is_integer(Count) andalso Count >= 0) andalso + (Type =:= db orelse Type =:= shard orelse Type =:= doc orelse Type =:= doc_write) -> - AtLimit = atomics:sub_get(Ref, map_get(Type, ?INDICES), 1) =< 0, + AtLimit = atomics:sub_get(Ref, map_get(Type, ?INDICES), Count) =< 0, {Backoff, TStamp} = map_get(Type, Backoffs), NowMSec = erlang:monotonic_time(millisecond), case NowMSec - TStamp > ?SENSITIVITY_MSEC of @@ -142,6 +166,7 @@ refill(#st{ref = Ref} = St) -> ok = atomics:put(Ref, map_get(db, ?INDICES), db_limit()), ok = atomics:put(Ref, map_get(shard, ?INDICES), shard_limit()), ok = atomics:put(Ref, map_get(doc, ?INDICES), doc_limit()), + ok = atomics:put(Ref, map_get(doc_write, ?INDICES), doc_write_limit()), schedule_refill(St). update_backoff(true, 0) -> @@ -160,6 +185,9 @@ shard_limit() -> doc_limit() -> cfg_int("doc_rate_limit", ?DOC_RATE_DEFAULT). +doc_write_limit() -> + cfg_int("doc_write_rate_limit", ?DOC_WRITE_RATE_DEFAULT). + cfg_int(Key, Default) when is_list(Key), is_integer(Default) -> config:get_integer("couch_scanner", Key, Default). @@ -175,6 +203,7 @@ couch_scanner_rate_limiter_test_() -> [ ?TDEF_FE(t_init), ?TDEF_FE(t_update), + ?TDEF_FE(t_update_multiple), ?TDEF_FE(t_refill) ] }. @@ -184,7 +213,8 @@ t_init(_) -> ?assertEqual(ok, refill()), ?assertMatch({Val, #client_st{}} when is_number(Val), update(ClientSt, db)), ?assertMatch({Val, #client_st{}} when is_number(Val), update(ClientSt, shard)), - ?assertMatch({Val, #client_st{}} when is_number(Val), update(ClientSt, doc)). + ?assertMatch({Val, #client_st{}} when is_number(Val), update(ClientSt, doc)), + ?assertMatch({Val, #client_st{}} when is_number(Val), update(ClientSt, doc_write)). t_update(_) -> ClientSt = ?MODULE:get(), @@ -196,6 +226,16 @@ t_update(_) -> {Backoff, _} = update(ClientSt1, db), ?assertEqual(?MAX_BACKOFF, Backoff). +t_update_multiple(_) -> + ClientSt = ?MODULE:get(), + Fun = fun(_, Acc) -> + {_, Acc1} = update(Acc, doc_write, 100), + reset_time(Acc1, doc_write) + end, + ClientSt1 = lists:foldl(Fun, ClientSt, lists:seq(1, 50)), + {Backoff, _} = update(ClientSt1, doc_write, 100), + ?assertEqual(?MAX_BACKOFF, Backoff). + t_refill(_) -> ClientSt = ?MODULE:get(), Fun = fun(_, Acc) -> diff --git a/src/couch_scanner/src/couch_scanner_server.erl b/src/couch_scanner/src/couch_scanner_server.erl index 7176173d21..2ec1ac5408 100644 --- a/src/couch_scanner/src/couch_scanner_server.erl +++ b/src/couch_scanner/src/couch_scanner_server.erl @@ -252,6 +252,8 @@ sched_exit_update(Id, #sched{} = Sched, {shutdown, reset}) -> couch_log:warning("~p : resetting plugin ~s", [?MODULE, Id]), couch_scanner_checkpoint:reset(Id), Sched#sched{start_time = 0, error_count = 0, reschedule = tsec()}; +sched_exit_update(_Id, #sched{} = Sched, {shutdown, stop}) -> + Sched#sched{start_time = 0, error_count = 0}; sched_exit_update(Id, #sched{} = Sched, Norm) when Norm == shutdown; Norm == normal -> diff --git a/src/couch_scanner/src/couch_scanner_util.erl b/src/couch_scanner/src/couch_scanner_util.erl index ff4edafd35..5dd44248c8 100644 --- a/src/couch_scanner/src/couch_scanner_util.erl +++ b/src/couch_scanner/src/couch_scanner_util.erl @@ -31,6 +31,7 @@ -define(DAY, 24 * ?HOUR). -define(WEEK, 7 * ?DAY). -define(MONTH, 30 * ?DAY). +-define(YEAR, 365 * ?DAY). new_scan_id() -> TSec = integer_to_binary(erlang:system_time(second)), @@ -240,15 +241,17 @@ parse_non_weekday_period(Period) -> undefined end. -parse_period_unit(Period) when is_list(Period) -> - case Period of - "sec" ++ _ -> ?SECOND; - "min" ++ _ -> ?MINUTE; - "hour" ++ _ -> ?HOUR; - "day" ++ _ -> ?DAY; - "week" ++ _ -> ?WEEK; - "month" ++ _ -> ?MONTH; - _ -> undefined +%% erlfmt-ignore +parse_period_unit(P) when is_list(P) -> + if + P == "s"; P == "sec"; P == "second"; P == "seconds" -> ?SECOND; + P == "min"; P == "minute"; P == "minutes" -> ?MINUTE; + P == "h"; P == "hrs"; P == "hour"; P == "hours" -> ?HOUR; + P == "d"; P == "day"; P == "days" -> ?DAY; + P == "w"; P == "week"; P == "weeks" -> ?WEEK; + P == "mon"; P == "month"; P == "months" -> ?MONTH; + P == "y"; P == "year"; P == "years" -> ?YEAR; + true -> undefined end. % Logging bits @@ -297,6 +300,9 @@ parse_after_test() -> parse_repeat_test() -> ?assertEqual(undefined, parse_repeat("foo")), ?assertEqual(undefined, parse_repeat("ReStarT")), + ?assertEqual(undefined, parse_repeat("1_ms")), + ?assertEqual(undefined, parse_repeat("1_x")), + ?assertEqual(undefined, parse_repeat("1_m")), ?assertEqual({weekday, 1}, parse_repeat("mon")), ?assertEqual({weekday, 1}, parse_repeat("Monday")), ?assertEqual({weekday, 2}, parse_repeat("tuesday")), @@ -306,16 +312,25 @@ parse_repeat_test() -> ?assertEqual({weekday, 6}, parse_repeat("sAt")), ?assertEqual({weekday, 7}, parse_repeat("sundays")), ?assertEqual(1, parse_repeat("1_sec")), + ?assertEqual(1, parse_repeat("1_s")), ?assertEqual(1, parse_repeat("1_second")), ?assertEqual(1, parse_repeat("1_sec")), ?assertEqual(2, parse_repeat("2_sec")), ?assertEqual(3, parse_repeat("3_seconds")), ?assertEqual(60, parse_repeat("1_min")), + ?assertEqual(60, parse_repeat("1_minute")), ?assertEqual(2 * 60, parse_repeat("2_minutes")), ?assertEqual(60 * 60, parse_repeat("1_hour")), + ?assertEqual(3 * 60 * 60, parse_repeat("3_hours")), + ?assertEqual(2 * 60 * 60, parse_repeat("2_h")), ?assertEqual(24 * 60 * 60, parse_repeat("1_day")), ?assertEqual(7 * 24 * 60 * 60, parse_repeat("1_week")), - ?assertEqual(30 * 24 * 60 * 60, parse_repeat("1_month")). + ?assertEqual(2 * 7 * 24 * 60 * 60, parse_repeat("2_weeks")), + ?assertEqual(30 * 24 * 60 * 60, parse_repeat("1_month")), + ?assertEqual(30 * 24 * 60 * 60, parse_repeat("1_mon")), + ?assertEqual(2 * 30 * 24 * 60 * 60, parse_repeat("2_months")), + ?assertEqual(365 * 24 * 60 * 60, parse_repeat("1_year")), + ?assertEqual(2 * 365 * 24 * 60 * 60, parse_repeat("2_year")). repeat_period_test() -> %Fri, May 31, 2024 16:08:37 diff --git a/src/couch_scanner/test/eunit/couch_scanner_test.erl b/src/couch_scanner/test/eunit/couch_scanner_test.erl index 6f7f3d8e2e..5d6a22f387 100644 --- a/src/couch_scanner/test/eunit/couch_scanner_test.erl +++ b/src/couch_scanner/test/eunit/couch_scanner_test.erl @@ -29,6 +29,7 @@ couch_scanner_test_() -> ?TDEF_FE(t_conflict_finder_works, 30), ?TDEF_FE(t_config_skips, 10), ?TDEF_FE(t_resume_after_error, 10), + ?TDEF_FE(t_resume_after_skip, 10), ?TDEF_FE(t_reset, 10), ?TDEF_FE(t_schedule_repeat, 10), ?TDEF_FE(t_schedule_after, 15) @@ -49,15 +50,23 @@ couch_scanner_test_() -> setup() -> {module, _} = code:ensure_loaded(?FIND_PLUGIN), meck:new(?FIND_PLUGIN, [passthrough]), + meck:new(fabric, [passthrough]), meck:new(couch_scanner_server, [passthrough]), meck:new(couch_scanner_util, [passthrough]), Ctx = test_util:start_couch([fabric, couch_scanner]), + % Run with the smallest batch size to exercise the batched + % ddoc iteration + config:set("couch_scanner", "ddoc_batch_size", "2", false), DbName1 = <<"dbname1", (?tempdb())/binary>>, DbName2 = <<"dbname2", (?tempdb())/binary>>, DbName3 = <<"dbname3", (?tempdb())/binary>>, ok = fabric:create_db(DbName1, [{q, "2"}, {n, "1"}]), ok = fabric:create_db(DbName2, [{q, "2"}, {n, "1"}]), ok = fabric:create_db(DbName3, [{q, "2"}, {n, "1"}]), + % Add a design doc the shards db. Scanner should ignore it. + {ok, _} = couch_util:with_db(mem3_sync:shards_db(), fun(Db) -> + couch_db:update_doc(Db, #doc{id = <<"_design/foo">>}, []) + end), ok = add_doc(DbName1, ?DOC1, #{foo1 => bar}), ok = add_doc(DbName1, ?DOC2, #{ foo2 => baz, @@ -84,8 +93,8 @@ setup() -> ok = add_doc(DbName2, ?DOC3, #{foo3 => bax}), ok = add_doc(DbName2, ?DOC4, #{foo4 => baw, <<>> => this_is_ok_apparently}), add_docs(DbName3, [ - {doc, ?DOC5, {2, [<<"x">>, <<"z">>]}, {[]}, [], false, []}, - {doc, ?DOC5, {2, [<<"y">>, <<"z">>]}, {[]}, [], false, []} + #doc{id = ?DOC5, revs = {2, [<<"x">>, <<"z">>]}, deleted = false}, + #doc{id = ?DOC5, revs = {2, [<<"y">>, <<"z">>]}, deleted = false} ]), couch_scanner:reset_checkpoints(), {Ctx, {DbName1, DbName2, DbName3}}. @@ -108,6 +117,7 @@ teardown({Ctx, {DbName1, DbName2, DbName3}}) -> fabric:delete_db(DbName1), fabric:delete_db(DbName2), fabric:delete_db(DbName3), + couch_server:delete(mem3_sync:shards_db(), [?ADMIN_CTX]), test_util:stop_couch(Ctx), meck:unload(). @@ -141,8 +151,6 @@ t_run_through_all_callbacks_basic({_, {DbName1, DbName2, _}}) -> ?assertEqual(2, num_calls(checkpoint, 1)), ?assertEqual(1, num_calls(db, ['_', DbName1])), ?assertEqual(1, num_calls(db, ['_', DbName2])), - ?assertEqual(1, num_calls(ddoc, ['_', DbName1, '_'])), - ?assertEqual(1, num_calls(ddoc, ['_', DbName2, '_'])), ?assert(num_calls(shards, 2) >= 2), DbOpenedCount = num_calls(db_opened, 2), ?assert(DbOpenedCount >= 4), @@ -161,10 +169,14 @@ t_find_reporting_works(_) -> config:set(Plugin ++ ".regexes", "foo14", "foo(1|4)", false), config:set(Plugin ++ ".regexes", "baz", "baz", false), meck:reset(couch_scanner_server), + meck:reset(fabric), config:set("couch_scanner_plugins", Plugin, "true", false), wait_exit(10000), % doc2 should have a baz and doc1 and doc4 matches foo14 - ?assertEqual(3, log_calls(warning)). + ?assertEqual(3, log_calls(warning)), + % check that we didn't call fabric:all_docs fetching design docs + % as we don't need to for this plugin + ?assertEqual(0, meck:num_calls(fabric, all_docs, 5)). t_ddoc_features_works({_, {_, DbName2, _}}) -> % Run the "ddoc_features" plugin @@ -201,11 +213,20 @@ t_conflict_finder_works({_, {_, _, DbName3}}) -> % Add a deleted conflicting doc to the third database. % 3 reports are expected: 2 doc reports and 1 db report. add_docs(DbName3, [ - {doc, ?DOC6, {2, [<<"x">>, <<"z">>]}, {[]}, [], false, []}, - {doc, ?DOC6, {2, [<<"d">>, <<"z">>]}, {[]}, [], true, []} + #doc{id = ?DOC6, revs = {2, [<<"x">>, <<"z">>]}, deleted = false}, + #doc{id = ?DOC6, revs = {2, [<<"d">>, <<"z">>]}, deleted = true} ]), resume_couch_scanner(Plugin), ?assertEqual(3, meck:num_calls(couch_scanner_util, log, LogArgs)), + % Should work even if all revs are deleted (the whole FDI is deleted) + add_docs(DbName3, [ + #doc{id = ?DOC6, revs = {3, [<<"a">>, <<"x">>, <<"z">>]}, deleted = true} + ]), + % Confirm it's deleted (we did the revs paths manipulations correctly) + ?assertEqual({not_found, deleted}, fabric:open_doc(DbName3, ?DOC6, [])), + % But we can still find the conflicts + resume_couch_scanner(Plugin), + ?assertEqual(3, meck:num_calls(couch_scanner_util, log, LogArgs)), % Set doc_report to false to only have 1 db report. config:set(Plugin, "doc_report", "false", false), resume_couch_scanner(Plugin), @@ -257,6 +278,25 @@ t_resume_after_error(_) -> config:set("couch_scanner_plugins", Plugin, "true", false), meck:wait(?FIND_PLUGIN, resume, 2, 10000). +t_resume_after_skip(_) -> + meck:reset(?FIND_PLUGIN), + meck:expect( + ?FIND_PLUGIN, + start, + 2, + meck:seq([ + skip, + meck:passthrough() + ]) + ), + Plugin = atom_to_list(?FIND_PLUGIN), + config:set("couch_scanner", "min_penalty_sec", "1", false), + config:set("couch_scanner", "interval_sec", "1", false), + config:set(Plugin, "repeat", "2_sec", false), + couch_scanner:resume(), + config:set("couch_scanner_plugins", Plugin, "true", false), + meck:wait(?FIND_PLUGIN, complete, 1, 10000). + t_reset(_) -> meck:reset(?FIND_PLUGIN), meck:expect( diff --git a/src/couch_stats/src/couch_stats_httpd.erl b/src/couch_stats/src/couch_stats_httpd.erl index 88ea169d06..5d69384e2f 100644 --- a/src/couch_stats/src/couch_stats_httpd.erl +++ b/src/couch_stats/src/couch_stats_httpd.erl @@ -99,8 +99,8 @@ extract_path([_ | _], _NotAnObject) -> maybe_format_key(Key) when is_list(Key) -> list_to_binary(Key); maybe_format_key(Key) when is_atom(Key) -> - list_to_binary(atom_to_list(Key)); + atom_to_binary(Key); maybe_format_key(Key) when is_integer(Key) -> - list_to_binary(integer_to_list(Key)); + integer_to_binary(Key); maybe_format_key(Key) when is_binary(Key) -> Key. diff --git a/src/couch_stats/src/couch_stats_process_tracker.erl b/src/couch_stats/src/couch_stats_process_tracker.erl index 33cc137da3..c9f9f12914 100644 --- a/src/couch_stats/src/couch_stats_process_tracker.erl +++ b/src/couch_stats/src/couch_stats_process_tracker.erl @@ -49,7 +49,7 @@ handle_call(Msg, _From, State) -> handle_cast({track, Pid, Name}, State) -> couch_stats:increment_counter(Name), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), ets:insert(?MODULE, {Ref, Name}), {noreply, State}; handle_cast(Msg, State) -> diff --git a/src/custodian/src/custodian_util.erl b/src/custodian/src/custodian_util.erl index 1a29ee7ad1..baf52dff7f 100644 --- a/src/custodian/src/custodian_util.erl +++ b/src/custodian/src/custodian_util.erl @@ -187,7 +187,7 @@ load_shards(Db, #full_doc_info{id = Id} = FDI) -> {ok, #doc{body = {Props}}} -> mem3_util:build_shards(Id, Props); {not_found, _} -> - erlang:error(database_does_not_exist, [Id]) + error(database_does_not_exist, [Id]) end. maybe_redirect(Nodes) -> diff --git a/src/ddoc_cache/src/ddoc_cache_entry.erl b/src/ddoc_cache/src/ddoc_cache_entry.erl index de9cb55cc0..694cafe8f9 100644 --- a/src/ddoc_cache/src/ddoc_cache_entry.erl +++ b/src/ddoc_cache/src/ddoc_cache_entry.erl @@ -75,16 +75,16 @@ start_link(Key, Default) -> {ok, Pid}. shutdown(Pid) -> - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), ok = gen_server:cast(Pid, shutdown), receive {'DOWN', Ref, process, Pid, normal} -> ok; {'DOWN', Ref, process, Pid, Reason} -> - erlang:exit(Reason) + exit(Reason) after ?ENTRY_SHUTDOWN_TIMEOUT -> - erlang:demonitor(Ref, [flush]), - erlang:exit({timeout, {entry_shutdown, Pid}}) + demonitor(Ref, [flush]), + exit({timeout, {entry_shutdown, Pid}}) end. open(Pid, Key) -> @@ -98,7 +98,7 @@ open(Pid, Key) -> end catch error:database_does_not_exist -> - erlang:error(database_does_not_exist); + error(database_does_not_exist); exit:_ -> % Its possible that this process was evicted just % before we tried talking to it. Just fallback @@ -257,7 +257,7 @@ handle_info(Msg, St) -> {stop, {bad_info, Msg}, St}. spawn_opener(Key) -> - {Pid, _} = erlang:spawn_monitor(?MODULE, do_open, [Key]), + {Pid, _} = spawn_monitor(?MODULE, do_open, [Key]), Pid. start_timer() -> @@ -269,10 +269,10 @@ start_timer() -> do_open(Key) -> try recover(Key) of Resp -> - erlang:exit({open_ok, Key, Resp}) + exit({open_ok, Key, Resp}) catch T:R:S -> - erlang:exit({open_error, Key, {T, R, S}}) + exit({open_error, Key, {T, R, S}}) end. update_lru(#st{key = Key, ts = Ts} = St) -> diff --git a/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl index 54f5c673f5..5d2e50ef2f 100644 --- a/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl +++ b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl @@ -26,7 +26,18 @@ ddocid(_) -> no_ddocid. recover(DbName) -> - {ok, DDocs} = fabric:design_docs(mem3:dbname(DbName)), + %% The VDU function is used to validate documents update before + %% storing them in the database. + %% Raise an error when invalid instead of returning an empty list. + DDocs = + case fabric:design_docs(mem3:dbname(DbName)) of + {ok, Resp} when is_list(Resp) -> + Resp; + {ok, Error} -> + error(Error); + {error, Error} -> + error(Error) + end, Funs = lists:flatmap( fun(DDoc) -> case couch_doc:get_validate_doc_fun(DbName, DDoc) of diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl index 915f3e45b5..b3ed6b83f5 100644 --- a/src/ddoc_cache/src/ddoc_cache_lru.erl +++ b/src/ddoc_cache/src/ddoc_cache_lru.erl @@ -273,7 +273,7 @@ remove_key(#{} = Dbs, Key) -> end. unlink_and_flush(Pid) -> - erlang:unlink(Pid), + unlink(Pid), % Its possible that the entry process has already exited before % we unlink it so we have to flush out a possible 'EXIT' % message sitting in our message queue. Notice that we're diff --git a/src/ddoc_cache/test/eunit/ddoc_cache_coverage_test.erl b/src/ddoc_cache/test/eunit/ddoc_cache_coverage_test.erl index a30f3b2ce2..f168c6c5cd 100644 --- a/src/ddoc_cache/test/eunit/ddoc_cache_coverage_test.erl +++ b/src/ddoc_cache/test/eunit/ddoc_cache_coverage_test.erl @@ -37,7 +37,7 @@ stop_on_evictor_death() -> Lru = whereis(ddoc_cache_lru), State = sys:get_state(Lru), Evictor = element(4, State), - Ref = erlang:monitor(process, Lru), + Ref = monitor(process, Lru), exit(Evictor, shutdown), receive {'DOWN', Ref, _, _, Reason} -> @@ -61,7 +61,7 @@ send_bad_messages(Name) -> end). wait_for_restart(Server, Fun) -> - Ref = erlang:monitor(process, whereis(Server)), + Ref = monitor(process, whereis(Server)), Fun(), receive {'DOWN', Ref, _, _, _} -> diff --git a/src/ddoc_cache/test/eunit/ddoc_cache_entry_test.erl b/src/ddoc_cache/test/eunit/ddoc_cache_entry_test.erl index 19ae24094b..8cc14d1c0d 100644 --- a/src/ddoc_cache/test/eunit/ddoc_cache_entry_test.erl +++ b/src/ddoc_cache/test/eunit/ddoc_cache_entry_test.erl @@ -53,7 +53,7 @@ cancel_and_replace_opener(_) -> true = ets:insert_new(?CACHE, #entry{key = Key}), {ok, Entry} = ddoc_cache_entry:start_link(Key, undefined), Opener1 = element(4, sys:get_state(Entry)), - Ref1 = erlang:monitor(process, Opener1), + Ref1 = monitor(process, Opener1), gen_server:cast(Entry, force_refresh), receive {'DOWN', Ref1, _, _, _} -> ok @@ -102,7 +102,7 @@ evict_when_not_accessed(_) -> Key = {ddoc_cache_entry_custom, {<<"bar">>, ?MODULE}}, true = ets:insert_new(?CACHE, #entry{key = Key}), {ok, Entry} = ddoc_cache_entry:start_link(Key, undefined), - Ref = erlang:monitor(process, Entry), + Ref = monitor(process, Entry), AccessCount1 = element(7, sys:get_state(Entry)), ?assertEqual(1, AccessCount1), ok = gen_server:cast(Entry, refresh), diff --git a/src/ddoc_cache/test/eunit/ddoc_cache_lru_test.erl b/src/ddoc_cache/test/eunit/ddoc_cache_lru_test.erl index 81285e8551..e89d24d4ec 100644 --- a/src/ddoc_cache/test/eunit/ddoc_cache_lru_test.erl +++ b/src/ddoc_cache/test/eunit/ddoc_cache_lru_test.erl @@ -91,7 +91,7 @@ check_multi_start(_) -> ), [#entry{pid = Pid}] = ets:tab2list(?CACHE), Opener = element(4, sys:get_state(Pid)), - OpenerRef = erlang:monitor(process, Opener), + OpenerRef = monitor(process, Opener), ?assert(is_process_alive(Opener)), Opener ! go, receive @@ -135,7 +135,7 @@ check_multi_open(_) -> ), [#entry{pid = Pid}] = ets:tab2list(?CACHE), Opener = element(4, sys:get_state(Pid)), - OpenerRef = erlang:monitor(process, Opener), + OpenerRef = monitor(process, Opener), ?assert(is_process_alive(Opener)), Opener ! go, receive @@ -162,7 +162,7 @@ check_capped_size(_) -> meck:reset(ddoc_cache_ev), lists:foreach( fun(I) -> - DbName = list_to_binary("big_" ++ integer_to_list(I)), + DbName = <<"bin_", (integer_to_binary(I))/binary>>, ddoc_cache:open_custom(DbName, ?MODULE), meck:wait(I, ddoc_cache_ev, event, [started, '_'], ?EVENT_TIMEOUT), ?assert(cache_size() < MaxSize * 2) @@ -171,7 +171,7 @@ check_capped_size(_) -> ), lists:foreach( fun(I) -> - DbName = list_to_binary("big_" ++ integer_to_list(I)), + DbName = <<"bin_", (integer_to_binary(I))/binary>>, ddoc_cache:open_custom(DbName, ?MODULE), meck:wait(I, ddoc_cache_ev, event, [started, '_'], ?EVENT_TIMEOUT), ?assert(cache_size() < MaxSize * 2) @@ -184,7 +184,7 @@ check_cache_refill({DbName, _}) -> meck:reset(ddoc_cache_ev), InitDDoc = fun(I) -> - NumBin = list_to_binary(integer_to_list(I)), + NumBin = integer_to_binary(I), DDocId = <<"_design/", NumBin/binary>>, Doc = #doc{id = DDocId, body = {[]}}, {ok, _} = fabric:update_doc(DbName, Doc, [?ADMIN_CTX]), @@ -242,7 +242,7 @@ check_evict_and_exit(_) -> ?assertEqual({ok, <<"dbname">>}, ddoc_cache_lru:open(Key)), [#entry{key = Key, pid = Pid}] = ets:tab2list(?CACHE), - erlang:monitor(process, whereis(ddoc_cache_lru)), + monitor(process, whereis(ddoc_cache_lru)), % Pause the LRU so we can queue multiple messages erlang:suspend_process(whereis(ddoc_cache_lru)), diff --git a/src/ddoc_cache/test/eunit/ddoc_cache_open_error_test.erl b/src/ddoc_cache/test/eunit/ddoc_cache_open_error_test.erl index a92f898be8..15f7461afe 100644 --- a/src/ddoc_cache/test/eunit/ddoc_cache_open_error_test.erl +++ b/src/ddoc_cache/test/eunit/ddoc_cache_open_error_test.erl @@ -18,7 +18,7 @@ start_couch() -> Ctx = ddoc_cache_tutil:start_couch(), meck:expect(fabric, open_doc, fun(_, ?FOOBAR, _) -> - erlang:error(test_kaboom) + error(test_kaboom) end), Ctx. diff --git a/src/ddoc_cache/test/eunit/ddoc_cache_open_test.erl b/src/ddoc_cache/test/eunit/ddoc_cache_open_test.erl index 778ef6cbb6..59e7d6311f 100644 --- a/src/ddoc_cache/test/eunit/ddoc_cache_open_test.erl +++ b/src/ddoc_cache/test/eunit/ddoc_cache_open_test.erl @@ -29,7 +29,7 @@ ddocid(_) -> no_ddocid. recover({deleted, _DbName}) -> - erlang:error(database_does_not_exist); + error(database_does_not_exist); recover(DbName) -> ddoc_cache_entry_validation_funs:recover(DbName). diff --git a/src/ddoc_cache/test/eunit/ddoc_cache_remove_test.erl b/src/ddoc_cache/test/eunit/ddoc_cache_remove_test.erl index 3186bbd631..dd5638dbbb 100644 --- a/src/ddoc_cache/test/eunit/ddoc_cache_remove_test.erl +++ b/src/ddoc_cache/test/eunit/ddoc_cache_remove_test.erl @@ -29,7 +29,7 @@ recover(DbName) -> <<"not_ok">> -> {ruh, roh}; <<"error">> -> - erlang:error(thpppt) + error(thpppt) end. start_couch() -> @@ -193,7 +193,7 @@ do_compact(ShardName) -> {ok, Db} = couch_db:open_int(ShardName, []), try {ok, Pid} = couch_db:start_compact(Db), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), receive {'DOWN', Ref, _, _, _} -> ok diff --git a/src/ddoc_cache/test/eunit/ddoc_cache_tutil.erl b/src/ddoc_cache/test/eunit/ddoc_cache_tutil.erl index 156472265e..5e2efbffac 100644 --- a/src/ddoc_cache/test/eunit/ddoc_cache_tutil.erl +++ b/src/ddoc_cache/test/eunit/ddoc_cache_tutil.erl @@ -50,7 +50,7 @@ clear() -> application:start(ddoc_cache). get_rev(DbName, DDocId) -> - {_, Ref} = erlang:spawn_monitor(fun() -> + {_, Ref} = spawn_monitor(fun() -> {ok, #doc{revs = Revs}} = fabric:open_doc(DbName, DDocId, [?ADMIN_CTX]), {Depth, [RevId | _]} = Revs, exit({Depth, RevId}) diff --git a/src/docs/src/api/database/changes.rst b/src/docs/src/api/database/changes.rst index b52ee161a1..7a5334e2fd 100644 --- a/src/docs/src/api/database/changes.rst +++ b/src/docs/src/api/database/changes.rst @@ -108,8 +108,9 @@ the filtering criteria. :query number timeout: Maximum period in *milliseconds* to wait for a change before the response is sent, even if there are no results. - Only applicable for :ref:`longpoll ` or - :ref:`continuous ` feeds. + Only applicable for :ref:`longpoll `, + :ref:`continuous ` or + :ref:`eventsource ` feeds. Default value is specified by :config:option:`chttpd/changes_timeout` configuration option. Note that ``60000`` value is also the default maximum timeout to prevent undetected dead connections. diff --git a/src/docs/src/api/server/authn.rst b/src/docs/src/api/server/authn.rst index 67e59722dd..7b89a87bde 100644 --- a/src/docs/src/api/server/authn.rst +++ b/src/docs/src/api/server/authn.rst @@ -516,3 +516,94 @@ https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). Note that you don't need to request :ref:`session ` to be authenticated by this method if the required HTTP header is provided. + +Two-Factor Authentication (2FA) +=============================== + +CouchDB supports built-in time-based one-time password (TOTP) authentication, so that 2FA +can be enabled for any user without extra plugins or tools. Here’s how it can be set up. + +Setting up 2FA in CouchDB +------------------------- + +1. Generate random token key + +A random `base32`_ string is generated and used as the user’s TOTP secret. For `example`_, +the following command produces a secure, random key: + +.. _base32: https://en.wikipedia.org/wiki/Base32 +.. _example: https://support.yubico.com/hc/en-us/articles/360015668699-Generating-Base32-string-examples + +.. code-block:: bash + + LC_ALL=C tr -dc 'A-Z2-7' ` in replication documents. ``doc_ids``, ``filter``, and ``selector`` are mutually exclusive. :` for details. Confirms that the server is up, running, and ready to respond to requests. If :config:option:`maintenance_mode ` is - ``true`` or ``nolb``, the endpoint will return a 404 response. + ``true`` or ``nolb``, the endpoint will return a 404 response. The status field + in the response body also changes to reflect the current ``maintenance_mode`` + defaulting to ``ok``. + + If :config:option:`maintenance_mode ` is ``true`` the status + field is set to ``maintenance_mode``. + + If :config:option:`maintenance_mode ` is set to ``nolb`` the + status field is set to ``nolb``. :>header Content-Type: :mimetype:`application/json` :code 200: Request completed successfully diff --git a/src/docs/src/cluster/databases.rst b/src/docs/src/cluster/databases.rst index 1dcbc2ba81..8441d8026a 100644 --- a/src/docs/src/cluster/databases.rst +++ b/src/docs/src/cluster/databases.rst @@ -66,6 +66,9 @@ Add a key value pair of the form: "zone": "metro-dc-a" +Alternatively, you can set the ``COUCHDB_ZONE`` environment variable +on each node and CouchDB will configure this document for you on startup. + Do this for all of the nodes in your cluster. In your config file (``local.ini`` or ``default.ini``) on each node, define a diff --git a/src/docs/src/cluster/purging.rst b/src/docs/src/cluster/purging.rst index cc9087d897..af5d7556f4 100644 --- a/src/docs/src/cluster/purging.rst +++ b/src/docs/src/cluster/purging.rst @@ -141,10 +141,8 @@ These settings can be updated in the default.ini or local.ini: +-----------------------+--------------------------------------------+----------+ | Field | Description | Default | +=======================+============================================+==========+ -| max_document_id_number| Allowed maximum number of documents in one | 100 | -| | purge request | | +-----------------------+--------------------------------------------+----------+ -| max_revisions_number | Allowed maximum number of accumulated | 1000 | +| max_revisions_number | Allowed maximum number of accumulated | infinity | | | revisions in one purge request | | +-----------------------+--------------------------------------------+----------+ | allowed_purge_seq_lag | Beside purged_infos_limit, allowed | 100 | diff --git a/src/docs/src/cluster/sharding.rst b/src/docs/src/cluster/sharding.rst index 7b8ca52ff0..e98df513db 100644 --- a/src/docs/src/cluster/sharding.rst +++ b/src/docs/src/cluster/sharding.rst @@ -650,6 +650,9 @@ Do this for all of the nodes in your cluster. For example: "zone": "{zone-name}" }' +Alternatively, you can set the ``COUCHDB_ZONE`` environment variable +on each node and CouchDB will configure this document for you on startup. + In the local config file (``local.ini``) of each node, define a consistent cluster-wide setting like: @@ -669,7 +672,7 @@ when the database is created, using the same syntax as the ini file: .. code-block:: bash - curl -X PUT $COUCH_URL:5984/{db}?zone={zone} + curl -X PUT $COUCH_URL:5984/{db}?placement={zone} The ``placement`` argument may also be specified. Note that this *will* override the logic that determines the number of created replicas! diff --git a/src/docs/src/cluster/troubleshooting.rst b/src/docs/src/cluster/troubleshooting.rst index 798902251e..16803cc805 100644 --- a/src/docs/src/cluster/troubleshooting.rst +++ b/src/docs/src/cluster/troubleshooting.rst @@ -97,7 +97,7 @@ the ``--level`` option: %MEM RSS 0.3 25116 - [debug] Local RPC: erlang:nodes([]) [5000] + [debug] Local RPC: nodes([]) [5000] [debug] Local RPC: mem3:nodes([]) [5000] [warning] Cluster member node3@127.0.0.1 is not connected to this node. Please check whether it is down. [info] Process is using 0.3% of available RAM, totalling 25116 KB of real memory. diff --git a/src/docs/src/config/misc.rst b/src/docs/src/config/misc.rst index ff46197535..6c9d6003e2 100644 --- a/src/docs/src/config/misc.rst +++ b/src/docs/src/config/misc.rst @@ -66,6 +66,7 @@ UUIDs Configuration .. config:option:: algorithm :: Generation Algorithm .. versionchanged:: 1.3 Added ``utc_id`` algorithm. + .. versionchanged:: 3.6 Added ``uuid_v7`` algorithm. CouchDB provides various algorithms to generate the UUID values that are used for document `_id`'s by default:: @@ -158,6 +159,25 @@ UUIDs Configuration ] } + - ``uuid_v7``: UUID v7 string in hex. + + .. code-block:: javascript + + { + "uuids": [ + "0199d2456e7f7b0a9b7130f9a9db8bee", + "0199d2456e7f72dda9f758fcc259c5fc", + "0199d2456e7f751c80b461180f7c7717", + "0199d2456e7f7c569b317d53367ca45a", + "0199d2456e7f77bfbffe92682c9c8c69", + "0199d2456e7f703ea97286f3d976343e", + "0199d2456e7f7f729142ed3b2da9101f", + "0199d2456e7f7723905c1f91f40d54f5", + "0199d2456e7f7e40979c7e2e22ffeb6a", + "0199d2456e7f7a42b43acfcc1e18eb84" + ] + } + .. note:: **Impact of UUID choices:** the choice of UUID has a significant impact on the layout of the B-tree, prior to compaction. @@ -293,26 +313,17 @@ Configuration of Database Purge .. config:section:: purge :: Configuration of Database Purge - .. config:option:: max_document_id_number :: Allowed number of documents \ - per Delete-Request - - .. versionadded:: 3.0 - - Sets the maximum number of documents allowed in a single purge request:: - - [purge] - max_document_id_number = 100 - .. config:option:: max_revisions_number :: Allowed number of accumulated \ revisions per Purge-Request .. versionadded:: 3.0 + .. versionchanged:: 3.6 Sets the maximum number of accumulated revisions allowed in a single purge request:: [purge] - max_revisions_number = 1000 + max_revisions_number = infinity .. config:option:: index_lag_warn_seconds :: Allowed duration for purge \ checkpoint document diff --git a/src/docs/src/config/query-servers.rst b/src/docs/src/config/query-servers.rst index 285bf12807..9932c49ab3 100644 --- a/src/docs/src/config/query-servers.rst +++ b/src/docs/src/config/query-servers.rst @@ -139,7 +139,9 @@ Query Servers Configuration .. config:option:: reduce_limit :: Reduce limit control Controls `Reduce overflow` error that raises when output of - :ref:`reduce functions ` is too big:: + :ref:`reduce functions ` is too big. The possible values are + ``true``, ``false`` or ``log``. The ``log`` value will log a warning + instead of crashing the view :: [query_server_config] reduce_limit = true @@ -148,6 +150,16 @@ Query Servers Configuration option since main propose of `reduce` functions is to *reduce* the input. + .. config:option:: reduce_limit_threshold :: Reduce limit threshold + + The number of bytes a reduce result must exceed to trigger the ``reduce_limit`` + control. Defaults to 5000. + + .. config:option:: reduce_limit_ratio :: Reduce limit ratio + + The ratio of input/output that must be exceeded to trigger the ``reduce_limit`` + control. Defaults to 2.0. + .. _config/native_query_servers: Native Erlang Query Server diff --git a/src/docs/src/config/replicator.rst b/src/docs/src/config/replicator.rst index 2fb3b2ca6f..bca107ae3b 100644 --- a/src/docs/src/config/replicator.rst +++ b/src/docs/src/config/replicator.rst @@ -303,7 +303,9 @@ Replicator Database Configuration .. config:option:: verify_ssl_certificates :: Check peer certificates - Set to true to validate peer certificates:: + Set to true to validate peer certificates. If + ``ssl_trusted_certificates_file`` is set it will be used, otherwise the + operating system CA files will be used:: [replicator] verify_ssl_certificates = false @@ -325,6 +327,18 @@ Replicator Database Configuration [replicator] ssl_certificate_max_depth = 3 + .. config:option:: cacert_reload_interval_hours :: CA certificates reload interval + + How often to reload operating system CA certificates (in hours). + Erlang VM caches OS CA certificates in memory after they are loaded + the first time. This setting specifies how often to clear the cache + and force reload certificate from disk. This can be useful if the VM + node is up for a long time, and the the CA certificate files are + updated using operating system packaging system during that time:: + + [replicator] + cacert_reload_interval_hours = 24 + .. config:option:: auth_plugins :: List of replicator client authentication plugins .. versionadded:: 2.2 diff --git a/src/docs/src/config/resharding.rst b/src/docs/src/config/resharding.rst index 91531aa7d3..968b24b498 100644 --- a/src/docs/src/config/resharding.rst +++ b/src/docs/src/config/resharding.rst @@ -21,7 +21,7 @@ Resharding Resharding Configuration ======================== -.. config:section:: resharding :: Resharding Configuration +.. config:section:: reshard :: Resharding Configuration .. config:option:: max_jobs :: Maximum resharding jobs per node diff --git a/src/docs/src/config/scanner.rst b/src/docs/src/config/scanner.rst index 4be3d9c06c..f36619f4ac 100644 --- a/src/docs/src/config/scanner.rst +++ b/src/docs/src/config/scanner.rst @@ -85,6 +85,25 @@ Scanner Options [couch_scanner] doc_rate_limit = 1000 + .. config:option:: doc_write_rate_limit + + Limit the rate at which plugins update documents. This rate limit + applies to plugins which explicitly use the + ``couch_scanner_rate_limiter`` module for rate limiting :: + + [couch_scanner] + doc_write_rate_limit = 500 + + .. config:option:: ddoc_batch_size + + Batch size to use when fetching design documents. For lots of small + design documents this value could be increased to 500 or 1000. If + design documents are large (100KB+) it could make sense to decrease it + a bit to 25 or 10. :: + + [couch_scanner] + ddoc_batch_size = 100 + .. config:section:: couch_scanner_plugins :: Enable Scanner Plugins .. config:option:: {plugin} diff --git a/src/docs/src/ddocs/ddocs.rst b/src/docs/src/ddocs/ddocs.rst index 4d3c06633f..fcb741e5fa 100644 --- a/src/docs/src/ddocs/ddocs.rst +++ b/src/docs/src/ddocs/ddocs.rst @@ -152,6 +152,9 @@ that the main task of reduce functions is to *reduce* the mapped result, not to make it bigger. Generally, your reduce function should converge rapidly to a single value - which could be an array or similar object. +Set ``reduce_limit`` to ``log`` so views which would crash if the setting were +``true`` would instead return the result and log an ``info`` level warning. + .. _reducefun/builtin: Built-in Reduce Functions diff --git a/src/docs/src/install/nouveau.rst b/src/docs/src/install/nouveau.rst index bfa40d160c..0dc031914f 100644 --- a/src/docs/src/install/nouveau.rst +++ b/src/docs/src/install/nouveau.rst @@ -107,21 +107,22 @@ if the Nouveau server's certificate is signed by a private CA. Configuring Nouveau to authenticate clients ------------------------------------------- -Nouveau is built on the dropwizard framework, which directly supports the `HTTPS -` transports. +Nouveau is built on the dropwizard framework, which directly supports the +`HTTPS `_ +transport. Acquiring or generating client and server certificates are out of scope of this documentation and we assume they have been created from here onward. We further assume the user can construct a Java keystore. -in ``nouveau.yaml`` you should remove all connectors of type ``http`` and add new -ones using ``https``; +in ``nouveau.yaml`` you should remove all connectors of type ``h2c`` and add new +ones using ``h2``; .. code-block:: yaml server: applicationConnectors: - - type: https + - type: h2 port: 5987 keyStorePath: keyStorePassword: diff --git a/src/docs/src/install/troubleshooting.rst b/src/docs/src/install/troubleshooting.rst index fc3b0986df..f8d13eefd2 100644 --- a/src/docs/src/install/troubleshooting.rst +++ b/src/docs/src/install/troubleshooting.rst @@ -228,6 +228,16 @@ unlimited. A detailed discussion can be found on the erlang-questions list, but the short answer is that you should decrease ``ulimit -n`` or lower the ``vm.args`` parameter ``+Q`` to something reasonable like 1024. +The same issue can be manifested in some docker configurations which default +ulimit max files to ``unlimited``. In those cases it's also recommended to set +a lower ``nofile`` ulimit, something like 65536. In docker compose files that +can be done as: + +.. code-block:: yaml + + ulimits: + nofiles: 65535 + Function raised exception (Cannot encode 'undefined' value as JSON) ------------------------------------------------------------------- If you see this in the CouchDB error logs, the JavaScript code you are using diff --git a/src/docs/src/install/unix.rst b/src/docs/src/install/unix.rst index 0d60ec52d9..616ef46e6c 100644 --- a/src/docs/src/install/unix.rst +++ b/src/docs/src/install/unix.rst @@ -34,7 +34,6 @@ to install CouchDB is to use the convenience binary packages: * CentOS/RHEL 9 (with caveats: depends on EPEL repository) * Debian 11 (bullseye) * Debian 12 (bookworm) -* Ubuntu 20.04 (focal) * Ubuntu 22.04 (jammy) * Ubuntu 24.04 (noble) @@ -157,7 +156,7 @@ Dependencies You should have the following installed: -* `Erlang OTP (25, 26, 27) `_ +* `Erlang OTP (26, 27, 28) `_ * `ICU `_ * `OpenSSL `_ * `Mozilla SpiderMonkey (1.8.5, 60, 68, 78, 91, 102, 115, 128) `_ diff --git a/src/docs/src/replication/protocol.rst b/src/docs/src/replication/protocol.rst index d08d7eae21..9f967f6ee7 100644 --- a/src/docs/src/replication/protocol.rst +++ b/src/docs/src/replication/protocol.rst @@ -280,7 +280,7 @@ and well handled: .. code-block:: http - HTTP/1.1 500 Internal Server Error + HTTP/1.1 401 Unauthorized Cache-Control: must-revalidate Content-Length: 108 Content-Type: application/json diff --git a/src/docs/src/setup/cluster.rst b/src/docs/src/setup/cluster.rst index a1aa087bcd..4e6a3fef98 100644 --- a/src/docs/src/setup/cluster.rst +++ b/src/docs/src/setup/cluster.rst @@ -303,7 +303,7 @@ After that we can join all the nodes together. Choose one node as the "setup coordination node" to run all these commands on. This "setup coordination node" only manages the setup and requires all other nodes to be able to see it and vice versa. *It has no special purpose beyond the setup process; CouchDB -does not have the concept of a "master" node in a cluster.* +does not have the concept of a primary node in a cluster.* Setup will not work with unavailable nodes. All nodes must be online and properly preconfigured before the cluster setup process can begin. diff --git a/src/dreyfus/src/clouseau_rpc.erl b/src/dreyfus/src/clouseau_rpc.erl index 12520da9a9..c036a5a9af 100644 --- a/src/dreyfus/src/clouseau_rpc.erl +++ b/src/dreyfus/src/clouseau_rpc.erl @@ -23,6 +23,7 @@ -export([analyze/2, version/0, disk_size/1]). -export([set_purge_seq/2, get_purge_seq/1, get_root_dir/0]). -export([connected/0]). +-export([check_service/1, check_service/2, check_services/0, check_services/1]). %% string represented as binary -type string_as_binary(_Value) :: nonempty_binary(). @@ -31,6 +32,7 @@ -type path() :: string_as_binary(_). -type error() :: any(). +-type throw(_Reason) :: no_return(). -type analyzer_name() :: string_as_binary(_). @@ -64,6 +66,8 @@ | {string_as_binary(stopwords), [field_name()]} ]. +-define(SEARCH_SERVICE_TIMEOUT, 2000). + -spec open_index(Peer :: pid(), Path :: shard(), Analyzer :: analyzer()) -> {ok, indexer_pid()} | error(). open_index(Peer, Path, Analyzer) -> @@ -258,10 +262,17 @@ rename(DbName) -> %% and an analyzer represented in a Javascript function in a design document. %% `Sig` is used to check if an index description is changed, %% and the index needs to be reconstructed. --spec cleanup(DbName :: string_as_binary(_), ActiveSigs :: [sig()]) -> +-spec cleanup(DbName :: string_as_binary(_), SigList :: list() | SigMap :: #{sig() => true}) -> ok. -cleanup(DbName, ActiveSigs) -> +% Compatibility clause to help when running search index cleanup during +% a mixed cluster state. Remove after version 3.6 +% +cleanup(DbName, SigList) when is_list(SigList) -> + SigMap = #{Sig => true || Sig <- SigList}, + cleanup(DbName, SigMap); +cleanup(DbName, #{} = SigMap) -> + ActiveSigs = maps:keys(SigMap), gen_server:cast({cleanup, clouseau()}, {cleanup, DbName, ActiveSigs}). %% a binary with value <<"tokens">> @@ -285,10 +296,25 @@ analyze(Analyzer, Text) -> version() -> rpc({main, clouseau()}, version). +-spec clouseau_major_vsn() -> binary() | throw({atom(), binary()}). +clouseau_major_vsn() -> + case version() of + {ok, <>} -> + <>; + {'EXIT', noconnection} -> + throw({noconnection, <<"Clouseau node is not connected.">>}); + {ok, null} -> + %% Backward compatibility: + %% If we run Clouseau from source code, remsh will return + %% `{ok, null}` for Clouseau <= 2.25.0. + %% See PR: https://github.com/cloudant-labs/clouseau/pull/106 + <<$2>> + end. + -spec connected() -> boolean(). connected() -> - HiddenNodes = erlang:nodes(hidden), + HiddenNodes = nodes(hidden), case lists:member(clouseau(), HiddenNodes) of true -> true; @@ -316,3 +342,61 @@ rpc(Ref, Msg) -> clouseau() -> list_to_atom(config:get("dreyfus", "name", "clouseau@127.0.0.1")). + +-type service() :: sup | main | analyzer | cleanup. +-type liveness_status() :: alive | timeout. + +-spec clouseau_services(MajorVsn) -> [service()] when + MajorVsn :: binary(). +clouseau_services(_MajorVsn) -> + [sup, main, analyzer, cleanup]. + +-spec is_valid(Service) -> boolean() when + Service :: atom(). +is_valid(Service) when is_atom(Service) -> + lists:member(Service, clouseau_services(clouseau_major_vsn())). + +-spec check_service(Service) -> Result when + Service :: service(), + Result :: {service(), liveness_status()} | throw({atom(), binary()}). +check_service(Service) -> + check_service(Service, ?SEARCH_SERVICE_TIMEOUT). + +-spec check_service(Service, Timeout) -> Result when + Service :: service(), + Timeout :: timeout(), + Result :: {service(), liveness_status()} | throw({atom(), binary()}). +check_service(Service, Timeout) when is_list(Service) -> + check_service(list_to_atom(Service), Timeout); +check_service(Service, Timeout) when is_atom(Service) -> + case is_valid(Service) of + true -> + Ref = make_ref(), + {Service, clouseau()} ! {ping, self(), Ref}, + receive + {pong, Ref} -> + {Service, alive} + after Timeout -> + {Service, timeout} + end; + false -> + NoService = atom_to_binary(Service), + throw({not_found, <<"no such service: ", NoService/binary>>}) + end. + +-spec check_services() -> Result when + Result :: [{service(), liveness_status()}] | throw({atom(), binary()}). +check_services() -> + check_services(?SEARCH_SERVICE_TIMEOUT). + +-spec check_services(Timeout) -> Result when + Timeout :: timeout(), + Result :: [{service(), liveness_status()}] | throw({atom(), binary()}). +check_services(Timeout) -> + case connected() of + true -> + Services = clouseau_services(clouseau_major_vsn()), + [check_service(S, Timeout) || S <- Services]; + false -> + throw({noconnection, <<"Clouseau node is not connected.">>}) + end. diff --git a/src/dreyfus/src/dreyfus_fabric_cleanup.erl b/src/dreyfus/src/dreyfus_fabric_cleanup.erl index e2710744d9..0488211be9 100644 --- a/src/dreyfus/src/dreyfus_fabric_cleanup.erl +++ b/src/dreyfus/src/dreyfus_fabric_cleanup.erl @@ -14,92 +14,50 @@ -module(dreyfus_fabric_cleanup). --include("dreyfus.hrl"). --include_lib("mem3/include/mem3.hrl"). --include_lib("couch/include/couch_db.hrl"). - --export([go/1]). +-export([go/1, go_local/3]). go(DbName) -> - {ok, DesignDocs} = fabric:design_docs(DbName), - ActiveSigs = lists:usort( - lists:flatmap( - fun active_sigs/1, - [couch_doc:from_json_obj(DD) || DD <- DesignDocs] - ) - ), - cleanup_local_purge_doc(DbName, ActiveSigs), - clouseau_rpc:cleanup(DbName, ActiveSigs), - ok. + case fabric_util:get_design_doc_records(DbName) of + {ok, DDocs} when is_list(DDocs) -> + Sigs = dreyfus_util:get_signatures_from_ddocs(DbName, DDocs), + Shards = mem3:shards(DbName), + ByNode = maps:groups_from_list(fun mem3:node/1, fun mem3:name/1, Shards), + Fun = fun(Node, Dbs, Acc) -> + erpc:send_request(Node, ?MODULE, go_local, [DbName, Dbs, Sigs], Node, Acc) + end, + Reqs = maps:fold(Fun, erpc:reqids_new(), ByNode), + recv(DbName, Reqs, fabric_util:abs_request_timeout()); + Error -> + couch_log:error("~p : error fetching ddocs db:~p ~p", [?MODULE, DbName, Error]), + Error + end. -active_sigs(#doc{body = {Fields}} = Doc) -> +% erpc endpoint for go/1 and fabric_index_cleanup:cleanup_indexes/2 +% +go_local(DbName, Dbs, #{} = Sigs) -> try - {RawIndexes} = couch_util:get_value(<<"indexes">>, Fields, {[]}), - {IndexNames, _} = lists:unzip(RawIndexes), - [ - begin - {ok, Index} = dreyfus_index:design_doc_to_index(Doc, IndexName), - Index#index.sig - end - || IndexName <- IndexNames - ] + lists:foreach( + fun(Db) -> + Checkpoints = dreyfus_util:get_purge_checkpoints(Db), + ok = couch_index_util:cleanup_purges(Db, Sigs, Checkpoints) + end, + Dbs + ), + clouseau_rpc:cleanup(DbName, Sigs), + ok catch - error:{badmatch, _Error} -> - [] + error:database_does_not_exist -> + ok end. -cleanup_local_purge_doc(DbName, ActiveSigs) -> - {ok, BaseDir} = clouseau_rpc:get_root_dir(), - DbNamePattern = <>, - Pattern0 = filename:join([BaseDir, "shards", "*", DbNamePattern, "*"]), - Pattern = binary_to_list(iolist_to_binary(Pattern0)), - DirListStrs = filelib:wildcard(Pattern), - DirList = [iolist_to_binary(DL) || DL <- DirListStrs], - LocalShards = mem3:local_shards(DbName), - ActiveDirs = lists:foldl( - fun(LS, AccOuter) -> - lists:foldl( - fun(Sig, AccInner) -> - DirName = filename:join([BaseDir, LS#shard.name, Sig]), - [DirName | AccInner] - end, - AccOuter, - ActiveSigs - ) - end, - [], - LocalShards - ), - - DeadDirs = DirList -- ActiveDirs, - lists:foreach( - fun(IdxDir) -> - Sig = dreyfus_util:get_signature_from_idxdir(IdxDir), - case Sig of - undefined -> - ok; - _ -> - DocId = dreyfus_util:get_local_purge_doc_id(Sig), - LocalShards = mem3:local_shards(DbName), - lists:foreach( - fun(LS) -> - ShardDbName = LS#shard.name, - {ok, ShardDb} = couch_db:open_int(ShardDbName, []), - case couch_db:open_doc(ShardDb, DocId, []) of - {ok, LocalPurgeDoc} -> - couch_db:update_doc( - ShardDb, - LocalPurgeDoc#doc{deleted = true}, - [?ADMIN_CTX] - ); - {not_found, _} -> - ok - end, - couch_db:close(ShardDb) - end, - LocalShards - ) - end - end, - DeadDirs - ). +recv(DbName, Reqs, Timeout) -> + case erpc:receive_response(Reqs, Timeout, true) of + {ok, _Lable, Reqs1} -> + recv(DbName, Reqs1, Timeout); + {Error, Label, Reqs1} -> + ErrMsg = "~p : error cleaning dreyfus indexes db:~p req:~p error:~p", + couch_log:error(ErrMsg, [?MODULE, DbName, Label, Error]), + recv(DbName, Reqs1, Timeout); + no_request -> + ok + end. diff --git a/src/dreyfus/src/dreyfus_fabric_group1.erl b/src/dreyfus/src/dreyfus_fabric_group1.erl index 9b08a94ebe..990d6d24e8 100644 --- a/src/dreyfus/src/dreyfus_fabric_group1.erl +++ b/src/dreyfus/src/dreyfus_fabric_group1.erl @@ -63,8 +63,8 @@ go(DbName, DDoc, IndexName, #index_query_args{} = QueryArgs) -> #shard.ref, fun handle_message/3, State, - infinity, - 1000 * 60 * 60 + fabric_util:timeout("search", "infinity"), + fabric_util:timeout("search_permsg", "3600000") ) after rexi_monitor:stop(RexiMon), diff --git a/src/dreyfus/src/dreyfus_fabric_group2.erl b/src/dreyfus/src/dreyfus_fabric_group2.erl index 3059aa30ee..613ac6555d 100644 --- a/src/dreyfus/src/dreyfus_fabric_group2.erl +++ b/src/dreyfus/src/dreyfus_fabric_group2.erl @@ -68,8 +68,8 @@ go(DbName, DDoc, IndexName, #index_query_args{} = QueryArgs) -> #shard.ref, fun handle_message/3, State, - infinity, - 1000 * 60 * 60 + fabric_util:timeout("search", "infinity"), + fabric_util:timeout("search_permsg", "3600000") ) after rexi_monitor:stop(RexiMon), diff --git a/src/dreyfus/src/dreyfus_fabric_search.erl b/src/dreyfus/src/dreyfus_fabric_search.erl index 75a2a5a3bf..0d07db6ef9 100644 --- a/src/dreyfus/src/dreyfus_fabric_search.erl +++ b/src/dreyfus/src/dreyfus_fabric_search.erl @@ -142,7 +142,9 @@ go(DbName, DDoc, IndexName, QueryArgs, Counters, Bookmark, RingOpts) -> {ok, Bookmark1, TotalHits, Hits1, Counts, Ranges} end; {error, Reason} -> - {error, Reason} + {error, Reason}; + {timeout, _State} -> + {error, timeout} after rexi_monitor:stop(RexiMon), fabric_streams:cleanup(Workers) diff --git a/src/dreyfus/src/dreyfus_index.erl b/src/dreyfus/src/dreyfus_index.erl index c97a837d51..5295a0065f 100644 --- a/src/dreyfus/src/dreyfus_index.erl +++ b/src/dreyfus/src/dreyfus_index.erl @@ -22,13 +22,13 @@ % public api. -export([ start_link/2, - design_doc_to_index/2, + design_doc_to_index/3, await/2, search/2, info/1, group1/2, group2/2, - design_doc_to_indexes/1 + design_doc_to_indexes/2 ]). % gen_server api. @@ -87,14 +87,14 @@ to_index_pid(Pid) -> false -> Pid end. -design_doc_to_indexes(#doc{body = {Fields}} = Doc) -> +design_doc_to_indexes(DbName, #doc{body = {Fields}} = Doc) -> RawIndexes = couch_util:get_value(<<"indexes">>, Fields, {[]}), case RawIndexes of {IndexList} when is_list(IndexList) -> {IndexNames, _} = lists:unzip(IndexList), lists:flatmap( fun(IndexName) -> - case (catch design_doc_to_index(Doc, IndexName)) of + case (catch design_doc_to_index(DbName, Doc, IndexName)) of {ok, #index{} = Index} -> [Index]; _ -> [] end @@ -301,7 +301,7 @@ open_index(DbName, #index{analyzer = Analyzer, sig = Sig}) -> Error end. -design_doc_to_index(#doc{id = Id, body = {Fields}}, IndexName) -> +design_doc_to_index(DbName, #doc{id = Id, body = {Fields}}, IndexName) -> Language = couch_util:get_value(<<"language">>, Fields, <<"javascript">>), {RawIndexes} = couch_util:get_value(<<"indexes">>, Fields, {[]}), InvalidDDocError = @@ -323,6 +323,7 @@ design_doc_to_index(#doc{id = Id, body = {Fields}}, IndexName) -> ) ), {ok, #index{ + dbname = DbName, analyzer = Analyzer, ddoc_id = Id, def = Def, diff --git a/src/dreyfus/src/dreyfus_rpc.erl b/src/dreyfus/src/dreyfus_rpc.erl index 2ebc5ffe58..3aed82d76c 100644 --- a/src/dreyfus/src/dreyfus_rpc.erl +++ b/src/dreyfus/src/dreyfus_rpc.erl @@ -17,6 +17,8 @@ -include("dreyfus.hrl"). -import(couch_query_servers, [get_os_process/1, ret_os_process/1, proc_prompt/2]). +-define(RETRY_DELAY, 2100). + % public api. -export([search/4, group1/4, group2/4, info/3, disk_size/3]). @@ -44,31 +46,34 @@ call(Fun, DbName, DDoc, IndexName, QueryArgs0) -> stale = Stale } = QueryArgs, {_LastSeq, MinSeq} = calculate_seqs(Db, Stale), - case dreyfus_index:design_doc_to_index(DDoc, IndexName) of + case dreyfus_index:design_doc_to_index(DbName, DDoc, IndexName) of {ok, Index} -> - case dreyfus_index_manager:get_index(DbName, Index) of - {ok, Pid} -> - case dreyfus_index:await(Pid, MinSeq) of - {ok, IndexPid, _Seq} -> - Result = dreyfus_index:Fun(IndexPid, QueryArgs), - rexi:reply(Result); - % obsolete clauses, remove after upgrade - ok -> - Result = dreyfus_index:Fun(Pid, QueryArgs), - rexi:reply(Result); - {ok, _Seq} -> - Result = dreyfus_index:Fun(Pid, QueryArgs), - rexi:reply(Result); - Error -> - rexi:reply(Error) - end; - Error -> - rexi:reply(Error) + try + rexi:reply(index_call(Fun, DbName, Index, QueryArgs, MinSeq)) + catch + exit:{noproc, _} -> + timer:sleep(?RETRY_DELAY), + %% try one more time to handle the case when Clouseau's LRU + %% closed the index in the middle of our call + rexi:reply(index_call(Fun, DbName, Index, QueryArgs, MinSeq)) end; Error -> rexi:reply(Error) end. +index_call(Fun, DbName, Index, QueryArgs, MinSeq) -> + case dreyfus_index_manager:get_index(DbName, Index) of + {ok, Pid} -> + case dreyfus_index:await(Pid, MinSeq) of + {ok, IndexPid, _Seq} -> + dreyfus_index:Fun(IndexPid, QueryArgs); + Error -> + Error + end; + Error -> + Error + end. + info(DbName, DDoc, IndexName) -> MFA = {?MODULE, info_int, [DbName, DDoc, IndexName]}, dreyfus_util:time([rpc, info], MFA). @@ -76,7 +81,7 @@ info(DbName, DDoc, IndexName) -> info_int(DbName, DDoc, IndexName) -> erlang:put(io_priority, {search, DbName}), check_interactive_mode(), - case dreyfus_index:design_doc_to_index(DDoc, IndexName) of + case dreyfus_index:design_doc_to_index(DbName, DDoc, IndexName) of {ok, Index} -> case dreyfus_index_manager:get_index(DbName, Index) of {ok, Pid} -> @@ -97,7 +102,7 @@ info_int(DbName, DDoc, IndexName) -> disk_size(DbName, DDoc, IndexName) -> erlang:put(io_priority, {search, DbName}), check_interactive_mode(), - case dreyfus_index:design_doc_to_index(DDoc, IndexName) of + case dreyfus_index:design_doc_to_index(DbName, DDoc, IndexName) of {ok, Index} -> Result = dreyfus_index_manager:get_disk_size(DbName, Index), rexi:reply(Result); diff --git a/src/dreyfus/src/dreyfus_util.erl b/src/dreyfus/src/dreyfus_util.erl index 301d3887ac..b8806c0893 100644 --- a/src/dreyfus/src/dreyfus_util.erl +++ b/src/dreyfus/src/dreyfus_util.erl @@ -25,9 +25,11 @@ ensure_local_purge_docs/2, get_value_from_options/2, get_local_purge_doc_id/1, + get_purge_checkpoints/1, get_local_purge_doc_body/4, maybe_create_local_purge_doc/2, maybe_create_local_purge_doc/3, + get_signatures_from_ddocs/2, get_signature_from_idxdir/1, verify_index_exists/2 ]). @@ -241,7 +243,7 @@ export(QueryArgs) -> time(Metric, {M, F, A}) when is_list(Metric) -> Start = os:timestamp(), try - erlang:apply(M, F, A) + apply(M, F, A) after Length = timer:now_diff(os:timestamp(), Start) / 1000, couch_stats:update_histogram([dreyfus | Metric], Length) @@ -305,7 +307,7 @@ ensure_local_purge_docs(DbName, DDocs) -> undefined -> false; _ -> - try dreyfus_index:design_doc_to_indexes(DDoc) of + try dreyfus_index:design_doc_to_indexes(DbName, DDoc) of SIndexes -> ensure_local_purge_doc(Db, SIndexes) catch _:_ -> @@ -360,6 +362,32 @@ maybe_create_local_purge_doc(Db, IndexPid, Index) -> get_local_purge_doc_id(Sig) -> ?l2b(?LOCAL_DOC_PREFIX ++ "purge-" ++ "dreyfus-" ++ Sig). +% Returns a map of `Sig => DocId` elements for all the purge view +% checkpoint docs. Sig is a hex-encoded binary. +% +get_purge_checkpoints(Db) -> + couch_index_util:get_purge_checkpoints(Db, <<"dreyfus">>). + +get_signatures_from_ddocs(DbName, DesignDocs) -> + SigList = lists:flatmap(fun(Doc) -> active_sigs(DbName, Doc) end, DesignDocs), + #{Sig => true || Sig <- SigList}. + +active_sigs(DbName, #doc{body = {Fields}} = Doc) -> + try + {RawIndexes} = couch_util:get_value(<<"indexes">>, Fields, {[]}), + {IndexNames, _} = lists:unzip(RawIndexes), + [ + begin + {ok, Index} = dreyfus_index:design_doc_to_index(DbName, Doc, IndexName), + Index#index.sig + end + || IndexName <- IndexNames + ] + catch + error:{badmatch, _Error} -> + [] + end. + get_signature_from_idxdir(IdxDir) -> IdxDirList = filename:split(IdxDir), Sig = lists:last(IdxDirList), @@ -415,7 +443,7 @@ verify_index_exists(DbName, Props) -> case couch_db:get_design_doc(Db, DDocId) of {ok, #doc{} = DDoc} -> {ok, IdxState} = dreyfus_index:design_doc_to_index( - DDoc, IndexName + DbName, DDoc, IndexName ), IdxState#index.sig == Sig; {not_found, _} -> diff --git a/src/dreyfus/test/eunit/dreyfus_purge_test.erl b/src/dreyfus/test/eunit/dreyfus_purge_test.erl index bed1f79f8c..a7c0068e01 100644 --- a/src/dreyfus/test/eunit/dreyfus_purge_test.erl +++ b/src/dreyfus/test/eunit/dreyfus_purge_test.erl @@ -1085,7 +1085,7 @@ wait_for_replicate(DbName, DocIds, ExpectRevCount, TimeOut) when wait_for_replicate(DbName, DocId, ExpectRevCount, TimeOut) -> FDI = fabric:get_full_doc_info(DbName, DocId, []), #doc_info{revs = Revs} = couch_doc:to_doc_info(FDI), - case erlang:length(Revs) of + case length(Revs) of ExpectRevCount -> couch_log:notice( "[~p] wait end by expect, time used:~p, DocId:~p", @@ -1102,17 +1102,17 @@ get_sigs(DbName) -> {ok, DesignDocs} = fabric:design_docs(DbName), lists:usort( lists:flatmap( - fun active_sigs/1, + fun(Doc) -> active_sigs(DbName, Doc) end, [couch_doc:from_json_obj(DD) || DD <- DesignDocs] ) ). -active_sigs(#doc{body = {Fields}} = Doc) -> +active_sigs(DbName, #doc{body = {Fields}} = Doc) -> {RawIndexes} = couch_util:get_value(<<"indexes">>, Fields, {[]}), {IndexNames, _} = lists:unzip(RawIndexes), [ begin - {ok, Index} = dreyfus_index:design_doc_to_index(Doc, IndexName), + {ok, Index} = dreyfus_index:design_doc_to_index(DbName, Doc, IndexName), Index#index.sig end || IndexName <- IndexNames diff --git a/src/ets_lru/src/ets_lru.erl b/src/ets_lru/src/ets_lru.erl index 080c8f7e2b..15a23f2773 100644 --- a/src/ets_lru/src/ets_lru.erl +++ b/src/ets_lru/src/ets_lru.erl @@ -349,7 +349,7 @@ next_timeout(Tab, Now, Max) -> infinity; {Time, _} -> TimeDiff = Now - Time, - erlang:max(Max - TimeDiff, 0) + max(Max - TimeDiff, 0) end. set_options(St, []) -> diff --git a/src/ets_lru/test/ets_lru_test.erl b/src/ets_lru/test/ets_lru_test.erl index 25a51fa9cd..6a7e5fcd0b 100644 --- a/src/ets_lru/test/ets_lru_test.erl +++ b/src/ets_lru/test/ets_lru_test.erl @@ -345,7 +345,7 @@ insert_kvs(Info, LRU, Count, Limit) -> timer:sleep(1), case ets:info(lru_objects, Info) > Limit of true -> - erlang:error(exceeded_limit); + error(exceeded_limit); false -> true end; @@ -355,7 +355,7 @@ insert_kvs(Info, LRU, Count, Limit) -> insert_kvs(Info, LRU, Count - 1, Limit). stop_lru({ok, LRU}) -> - Ref = erlang:monitor(process, LRU), + Ref = monitor(process, LRU), ets_lru:stop(LRU), receive {'DOWN', Ref, process, LRU, Reason} -> Reason diff --git a/src/exxhash/README.md b/src/exxhash/README.md index b99fa8605d..59ca6a2650 100644 --- a/src/exxhash/README.md +++ b/src/exxhash/README.md @@ -22,7 +22,7 @@ Updating xxHash was originally vendored from https://cyan4973.github.io/xxHash/ with commit SHA f4bef929aa854e9f52a303c5e58fd52855a0ecfa -Updated on 2025-04-30 from commit 41fea3d9ac7881c78fdc4003626977aa073bb906 +Updated on 2025-09-17 from commit c961fbe61ad1ee1e430b9c304735a0534fda1c6d Only these two files are used from the original library: `c_src/xxhash.h` diff --git a/src/exxhash/c_src/xxhash.h b/src/exxhash/c_src/xxhash.h index 66364b66f9..1b975455fa 100644 --- a/src/exxhash/c_src/xxhash.h +++ b/src/exxhash/c_src/xxhash.h @@ -791,18 +791,9 @@ XXH_PUBLIC_API XXH_PUREF XXH32_hash_t XXH32_hashFromCanonical(const XXH32_canoni #endif /*! @endcond */ -/*! @cond Doxygen ignores this part */ -/* - * C23 __STDC_VERSION__ number hasn't been specified yet. For now - * leave as `201711L` (C17 + 1). - * TODO: Update to correct value when its been specified. - */ -#define XXH_C23_VN 201711L -/*! @endcond */ - /*! @cond Doxygen ignores this part */ /* C-language Attributes are added in C23. */ -#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= XXH_C23_VN) && defined(__has_c_attribute) +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202311L) && defined(__has_c_attribute) # define XXH_HAS_C_ATTRIBUTE(x) __has_c_attribute(x) #else # define XXH_HAS_C_ATTRIBUTE(x) 0 @@ -1126,7 +1117,7 @@ XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH64_hashFromCanonical(XXH_NOESCAPE const # define XXH_SVE 6 /*!< SVE for some ARMv8-A and ARMv9-A */ # define XXH_LSX 7 /*!< LSX (128-bit SIMD) for LoongArch64 */ # define XXH_LASX 8 /*!< LASX (256-bit SIMD) for LoongArch64 */ - +# define XXH_RVV 9 /*!< RVV (RISC-V Vector) for RISC-V */ /*-********************************************************************** * XXH3 64-bit variant @@ -2661,7 +2652,7 @@ typedef union { xxh_u32 u32; } __attribute__((__packed__)) unalign; #endif static xxh_u32 XXH_read32(const void* ptr) { - typedef __attribute__((__aligned__(1))) xxh_u32 xxh_unalign32; + typedef __attribute__((__aligned__(1))) __attribute__((__may_alias__)) xxh_u32 xxh_unalign32; return *((const xxh_unalign32*)ptr); } @@ -2753,7 +2744,7 @@ static int XXH_isLittleEndian(void) * additional case: * * ``` - * #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= XXH_C23_VN) + * #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202311L) * # include * # ifdef unreachable * # define XXH_UNREACHABLE() unreachable() @@ -3374,7 +3365,7 @@ typedef union { xxh_u32 u32; xxh_u64 u64; } __attribute__((__packed__)) unalign6 #endif static xxh_u64 XXH_read64(const void* ptr) { - typedef __attribute__((__aligned__(1))) xxh_u64 xxh_unalign64; + typedef __attribute__((__aligned__(1))) __attribute__((__may_alias__)) xxh_u64 xxh_unalign64; return *((const xxh_unalign64*)ptr); } @@ -3882,6 +3873,8 @@ XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(XXH_NOESCAPE const XXH64_can # include # elif defined(__loongarch_sx) # include +# elif defined(__riscv_vector) +# include # endif #endif @@ -4020,6 +4013,8 @@ XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(XXH_NOESCAPE const XXH64_can # define XXH_VECTOR XXH_LASX # elif defined(__loongarch_sx) # define XXH_VECTOR XXH_LSX +# elif defined(__riscv_vector) +# define XXH_VECTOR XXH_RVV # else # define XXH_VECTOR XXH_SCALAR # endif @@ -4061,6 +4056,8 @@ XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(XXH_NOESCAPE const XXH64_can # define XXH_ACC_ALIGN 64 # elif XXH_VECTOR == XXH_LSX /* lsx */ # define XXH_ACC_ALIGN 64 +# elif XXH_VECTOR == XXH_RVV /* rvv */ +# define XXH_ACC_ALIGN 64 /* could be 8, but 64 may be faster */ # endif #endif @@ -4069,6 +4066,8 @@ XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(XXH_NOESCAPE const XXH64_can # define XXH_SEC_ALIGN XXH_ACC_ALIGN #elif XXH_VECTOR == XXH_SVE # define XXH_SEC_ALIGN XXH_ACC_ALIGN +#elif XXH_VECTOR == XXH_RVV +# define XXH_SEC_ALIGN XXH_ACC_ALIGN #else # define XXH_SEC_ALIGN 8 #endif @@ -5273,10 +5272,18 @@ XXH_FORCE_INLINE XXH_TARGET_SSE2 void XXH3_initCustomSecret_sse2(void* XXH_RESTR (void)(&XXH_writeLE64); { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / sizeof(__m128i); -# if defined(_MSC_VER) && defined(_M_IX86) && _MSC_VER < 1900 - /* MSVC 32bit mode does not support _mm_set_epi64x before 2015 */ - XXH_ALIGN(16) const xxh_i64 seed64x2[2] = { (xxh_i64)seed64, (xxh_i64)(0U - seed64) }; - __m128i const seed = _mm_load_si128((__m128i const*)seed64x2); +# if defined(_MSC_VER) && defined(_M_IX86) && _MSC_VER <= 1900 + /* MSVC 32bit mode does not support _mm_set_epi64x before 2015 + * and some specific variants of 2015 may also lack it */ + /* Cast to unsigned 64-bit first to avoid signed arithmetic issues */ + xxh_u64 const seed64_unsigned = (xxh_u64)seed64; + xxh_u64 const neg_seed64 = (xxh_u64)(0ULL - seed64_unsigned); + __m128i const seed = _mm_set_epi32( + (int)(neg_seed64 >> 32), /* high 32 bits of negated seed */ + (int)(neg_seed64), /* low 32 bits of negated seed */ + (int)(seed64_unsigned >> 32), /* high 32 bits of original seed */ + (int)(seed64_unsigned) /* low 32 bits of original seed */ + ); # else __m128i const seed = _mm_set_epi64x((xxh_i64)(0U - seed64), (xxh_i64)seed64); # endif @@ -5714,8 +5721,9 @@ XXH3_accumulate_512_lsx( void* XXH_RESTRICT acc, __m128i* const xacc = (__m128i *) acc; const __m128i* const xinput = (const __m128i *) input; const __m128i* const xsecret = (const __m128i *) secret; + size_t i; - for (size_t i = 0; i < XXH_STRIPE_LEN / sizeof(__m128i); i++) { + for (i = 0; i < XXH_STRIPE_LEN / sizeof(__m128i); i++) { /* data_vec = xinput[i]; */ __m128i const data_vec = __lsx_vld(xinput + i, 0); /* key_vec = xsecret[i]; */ @@ -5745,8 +5753,9 @@ XXH3_scrambleAcc_lsx(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) __m128i* const xacc = (__m128i*) acc; const __m128i* const xsecret = (const __m128i *) secret; const __m128i prime32 = __lsx_vreplgr2vr_d(XXH_PRIME32_1); + size_t i; - for (size_t i = 0; i < XXH_STRIPE_LEN / sizeof(__m128i); i++) { + for (i = 0; i < XXH_STRIPE_LEN / sizeof(__m128i); i++) { /* xacc[i] ^= (xacc[i] >> 47) */ __m128i const acc_vec = xacc[i]; __m128i const shifted = __lsx_vsrli_d(acc_vec, 47); @@ -5773,11 +5782,12 @@ XXH3_accumulate_512_lasx( void* XXH_RESTRICT acc, { XXH_ASSERT((((size_t)acc) & 31) == 0); { + size_t i; __m256i* const xacc = (__m256i *) acc; const __m256i* const xinput = (const __m256i *) input; const __m256i* const xsecret = (const __m256i *) secret; - for (size_t i = 0; i < XXH_STRIPE_LEN / sizeof(__m256i); i++) { + for (i = 0; i < XXH_STRIPE_LEN / sizeof(__m256i); i++) { /* data_vec = xinput[i]; */ __m256i const data_vec = __lasx_xvld(xinput + i, 0); /* key_vec = xsecret[i]; */ @@ -5807,8 +5817,9 @@ XXH3_scrambleAcc_lasx(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) __m256i* const xacc = (__m256i*) acc; const __m256i* const xsecret = (const __m256i *) secret; const __m256i prime32 = __lasx_xvreplgr2vr_d(XXH_PRIME32_1); + size_t i; - for (size_t i = 0; i < XXH_STRIPE_LEN / sizeof(__m256i); i++) { + for (i = 0; i < XXH_STRIPE_LEN / sizeof(__m256i); i++) { /* xacc[i] ^= (xacc[i] >> 47) */ __m256i const acc_vec = xacc[i]; __m256i const shifted = __lasx_xvsrli_d(acc_vec, 47); @@ -5825,6 +5836,133 @@ XXH3_scrambleAcc_lasx(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) #endif +#if (XXH_VECTOR == XXH_RVV) + #define XXH_CONCAT2(X, Y) X ## Y + #define XXH_CONCAT(X, Y) XXH_CONCAT2(X, Y) +#if ((defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 13) || \ + (defined(__clang__) && __clang_major__ < 16)) + #define XXH_RVOP(op) op + #define XXH_RVCAST(op) XXH_CONCAT(vreinterpret_v_, op) +#else + #define XXH_RVOP(op) XXH_CONCAT(__riscv_, op) + #define XXH_RVCAST(op) XXH_CONCAT(__riscv_vreinterpret_v_, op) +#endif +XXH_FORCE_INLINE void +XXH3_accumulate_512_rvv( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 63) == 0); + { + // Try to set vector lenght to 512 bits. + // If this length is unavailable, then maximum available will be used + size_t vl = XXH_RVOP(vsetvl_e64m2)(8); + + uint64_t* xacc = (uint64_t*) acc; + const uint64_t* xinput = (const uint64_t*) input; + const uint64_t* xsecret = (const uint64_t*) secret; + static const uint64_t swap_mask[16] = {1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10, 13, 12, 15, 14}; + vuint64m2_t xswap_mask = XXH_RVOP(vle64_v_u64m2)(swap_mask, vl); + + size_t i; + for (i = 0; i < XXH_STRIPE_LEN/8; i += vl) { + /* data_vec = xinput[i]; */ + vuint64m2_t data_vec = XXH_RVCAST(u8m2_u64m2)(XXH_RVOP(vle8_v_u8m2)((const uint8_t*)(xinput + i), vl * 8)); + /* key_vec = xsecret[i]; */ + vuint64m2_t key_vec = XXH_RVCAST(u8m2_u64m2)(XXH_RVOP(vle8_v_u8m2)((const uint8_t*)(xsecret + i), vl * 8)); + /* acc_vec = xacc[i]; */ + vuint64m2_t acc_vec = XXH_RVOP(vle64_v_u64m2)(xacc + i, vl); + /* data_key = data_vec ^ key_vec; */ + vuint64m2_t data_key = XXH_RVOP(vxor_vv_u64m2)(data_vec, key_vec, vl); + /* data_key_hi = data_key >> 32; */ + vuint64m2_t data_key_hi = XXH_RVOP(vsrl_vx_u64m2)(data_key, 32, vl); + /* data_key_lo = data_key & 0xffffffff; */ + vuint64m2_t data_key_lo = XXH_RVOP(vand_vx_u64m2)(data_key, 0xffffffff, vl); + /* swap high and low halves */ + vuint64m2_t data_swap = XXH_RVOP(vrgather_vv_u64m2)(data_vec, xswap_mask, vl); + /* acc_vec += data_key_lo * data_key_hi; */ + acc_vec = XXH_RVOP(vmacc_vv_u64m2)(acc_vec, data_key_lo, data_key_hi, vl); + /* acc_vec += data_swap; */ + acc_vec = XXH_RVOP(vadd_vv_u64m2)(acc_vec, data_swap, vl); + /* xacc[i] = acc_vec; */ + XXH_RVOP(vse64_v_u64m2)(xacc + i, acc_vec, vl); + } + } +} + +XXH_FORCE_INLINE XXH3_ACCUMULATE_TEMPLATE(rvv) + +XXH_FORCE_INLINE void +XXH3_scrambleAcc_rvv(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + { + size_t count = XXH_STRIPE_LEN/8; + uint64_t* xacc = (uint64_t*)acc; + const uint8_t* xsecret = (const uint8_t *)secret; + size_t vl; + for (; count > 0; count -= vl, xacc += vl, xsecret += vl*8) { + vl = XXH_RVOP(vsetvl_e64m2)(count); + { + /* key_vec = xsecret[i]; */ + vuint64m2_t key_vec = XXH_RVCAST(u8m2_u64m2)(XXH_RVOP(vle8_v_u8m2)(xsecret, vl*8)); + /* acc_vec = xacc[i]; */ + vuint64m2_t acc_vec = XXH_RVOP(vle64_v_u64m2)(xacc, vl); + /* acc_vec ^= acc_vec >> 47; */ + vuint64m2_t vsrl = XXH_RVOP(vsrl_vx_u64m2)(acc_vec, 47, vl); + acc_vec = XXH_RVOP(vxor_vv_u64m2)(acc_vec, vsrl, vl); + /* acc_vec ^= key_vec; */ + acc_vec = XXH_RVOP(vxor_vv_u64m2)(acc_vec, key_vec, vl); + /* acc_vec *= XXH_PRIME32_1; */ + acc_vec = XXH_RVOP(vmul_vx_u64m2)(acc_vec, XXH_PRIME32_1, vl); + /* xacc[i] *= acc_vec; */ + XXH_RVOP(vse64_v_u64m2)(xacc, acc_vec, vl); + } + } + } +} + +XXH_FORCE_INLINE void +XXH3_initCustomSecret_rvv(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + XXH_STATIC_ASSERT(XXH_SEC_ALIGN >= 8); + XXH_ASSERT(((size_t)customSecret & 7) == 0); + (void)(&XXH_writeLE64); + { + size_t count = XXH_SECRET_DEFAULT_SIZE/8; + size_t vl; + size_t VLMAX = XXH_RVOP(vsetvlmax_e64m2)(); + int64_t* cSecret = (int64_t*)customSecret; + const int64_t* kSecret = (const int64_t*)(const void*)XXH3_kSecret; + +#if __riscv_v_intrinsic >= 1000000 + // ratified v1.0 intrinics version + vbool32_t mneg = XXH_RVCAST(u8m1_b32)( + XXH_RVOP(vmv_v_x_u8m1)(0xaa, XXH_RVOP(vsetvlmax_e8m1)())); +#else + // support pre-ratification intrinics, which lack mask to vector casts + size_t vlmax = XXH_RVOP(vsetvlmax_e8m1)(); + vbool32_t mneg = XXH_RVOP(vmseq_vx_u8mf4_b32)( + XXH_RVOP(vand_vx_u8mf4)( + XXH_RVOP(vid_v_u8mf4)(vlmax), 1, vlmax), 1, vlmax); +#endif + vint64m2_t seed = XXH_RVOP(vmv_v_x_i64m2)((int64_t)seed64, VLMAX); + seed = XXH_RVOP(vneg_v_i64m2_mu)(mneg, seed, seed, VLMAX); + + for (; count > 0; count -= vl, cSecret += vl, kSecret += vl) { + /* make sure vl=VLMAX until last iteration */ + vl = XXH_RVOP(vsetvl_e64m2)(count < VLMAX ? count : VLMAX); + { + vint64m2_t src = XXH_RVOP(vle64_v_i64m2)(kSecret, vl); + vint64m2_t res = XXH_RVOP(vadd_vv_i64m2)(src, seed, vl); + XXH_RVOP(vse64_v_i64m2)(cSecret, res, vl); + } + } + } +} +#endif + + /* scalar variants - universal */ #if defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) @@ -6067,6 +6205,12 @@ typedef void (*XXH3_f_initCustomSecret)(void* XXH_RESTRICT, xxh_u64); #define XXH3_scrambleAcc XXH3_scrambleAcc_lsx #define XXH3_initCustomSecret XXH3_initCustomSecret_scalar +#elif (XXH_VECTOR == XXH_RVV) +#define XXH3_accumulate_512 XXH3_accumulate_512_rvv +#define XXH3_accumulate XXH3_accumulate_rvv +#define XXH3_scrambleAcc XXH3_scrambleAcc_rvv +#define XXH3_initCustomSecret XXH3_initCustomSecret_rvv + #else /* scalar */ #define XXH3_accumulate_512 XXH3_accumulate_512_scalar @@ -6563,6 +6707,16 @@ XXH3_update(XXH3_state_t* XXH_RESTRICT const state, } XXH_ASSERT(state != NULL); + state->totalLen += len; + + /* small input : just fill in tmp buffer */ + XXH_ASSERT(state->bufferedSize <= XXH3_INTERNALBUFFER_SIZE); + if (len <= XXH3_INTERNALBUFFER_SIZE - state->bufferedSize) { + XXH_memcpy(state->buffer + state->bufferedSize, input, len); + state->bufferedSize += (XXH32_hash_t)len; + return XXH_OK; + } + { const xxh_u8* const bEnd = input + len; const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret; #if defined(XXH3_STREAM_USE_STACK) && XXH3_STREAM_USE_STACK >= 1 @@ -6575,15 +6729,6 @@ XXH3_update(XXH3_state_t* XXH_RESTRICT const state, #else xxh_u64* XXH_RESTRICT const acc = state->acc; #endif - state->totalLen += len; - XXH_ASSERT(state->bufferedSize <= XXH3_INTERNALBUFFER_SIZE); - - /* small input : just fill in tmp buffer */ - if (len <= XXH3_INTERNALBUFFER_SIZE - state->bufferedSize) { - XXH_memcpy(state->buffer + state->bufferedSize, input, len); - state->bufferedSize += (XXH32_hash_t)len; - return XXH_OK; - } /* total input is now > XXH3_INTERNALBUFFER_SIZE */ #define XXH3_INTERNALBUFFER_STRIPES (XXH3_INTERNALBUFFER_SIZE / XXH_STRIPE_LEN) diff --git a/src/fabric/src/fabric.erl b/src/fabric/src/fabric.erl index d552a387dd..9cd8a07a5d 100644 --- a/src/fabric/src/fabric.erl +++ b/src/fabric/src/fabric.erl @@ -25,6 +25,8 @@ get_db_info/1, get_doc_count/1, get_doc_count/2, set_revs_limit/3, + update_props/3, + update_props/4, set_security/2, set_security/3, get_revs_limit/1, get_security/1, get_security/2, @@ -62,9 +64,10 @@ -export([ design_docs/1, reset_validation_funs/1, - cleanup_index_files/0, - cleanup_index_files/1, + cleanup_index_files_all_nodes/0, cleanup_index_files_all_nodes/1, + cleanup_index_files_this_node/0, + cleanup_index_files_this_node/1, dbname/1, db_uuids/1 ]). @@ -175,6 +178,16 @@ get_revs_limit(DbName) -> catch couch_db:close(Db) end. +%% @doc update shard property. Some properties like `partitioned` or `hash` are +%% static and cannot be updated. They will return an error. +-spec update_props(dbname(), atom() | binary(), any()) -> ok. +update_props(DbName, K, V) -> + update_props(DbName, K, V, [?ADMIN_CTX]). + +-spec update_props(dbname(), atom() | binary(), any(), [option()]) -> ok. +update_props(DbName, K, V, Options) when is_atom(K) orelse is_binary(K) -> + fabric_db_meta:update_props(dbname(DbName), K, V, opts(Options)). + %% @doc sets the readers/writers/admin permissions for a database -spec set_security(dbname(), SecObj :: json_obj()) -> ok. set_security(DbName, SecObj) -> @@ -570,54 +583,17 @@ reset_validation_funs(DbName) -> || #shard{node = Node, name = Name} <- mem3:shards(DbName) ]. -%% @doc clean up index files for all Dbs --spec cleanup_index_files() -> [ok]. -cleanup_index_files() -> - {ok, Dbs} = fabric:all_dbs(), - [cleanup_index_files(Db) || Db <- Dbs]. +cleanup_index_files_this_node() -> + fabric_index_cleanup:cleanup_this_node(). -%% @doc clean up index files for a specific db --spec cleanup_index_files(dbname()) -> ok. -cleanup_index_files(DbName) -> - try - ShardNames = [mem3:name(S) || S <- mem3:local_shards(dbname(DbName))], - cleanup_local_indices_and_purge_checkpoints(ShardNames) - catch - error:database_does_not_exist -> - ok - end. +cleanup_index_files_this_node(Db) -> + fabric_index_cleanup:cleanup_this_node(dbname(Db)). -cleanup_local_indices_and_purge_checkpoints([]) -> - ok; -cleanup_local_indices_and_purge_checkpoints([_ | _] = Dbs) -> - AllIndices = lists:map(fun couch_mrview_util:get_index_files/1, Dbs), - AllPurges = lists:map(fun couch_mrview_util:get_purge_checkpoints/1, Dbs), - Sigs = couch_mrview_util:get_signatures(hd(Dbs)), - ok = cleanup_purges(Sigs, AllPurges, Dbs), - ok = cleanup_indices(Sigs, AllIndices). - -cleanup_purges(Sigs, AllPurges, Dbs) -> - Fun = fun(DbPurges, Db) -> - couch_mrview_cleanup:cleanup_purges(Db, Sigs, DbPurges) - end, - lists:zipwith(Fun, AllPurges, Dbs), - ok. +cleanup_index_files_all_nodes() -> + fabric_index_cleanup:cleanup_all_nodes(). -cleanup_indices(Sigs, AllIndices) -> - Fun = fun(DbIndices) -> - couch_mrview_cleanup:cleanup_indices(Sigs, DbIndices) - end, - lists:foreach(Fun, AllIndices). - -%% @doc clean up index files for a specific db on all nodes --spec cleanup_index_files_all_nodes(dbname()) -> [reference()]. -cleanup_index_files_all_nodes(DbName) -> - lists:foreach( - fun(Node) -> - rexi:cast(Node, {?MODULE, cleanup_index_files, [DbName]}) - end, - mem3:nodes() - ). +cleanup_index_files_all_nodes(Db) -> + fabric_index_cleanup:cleanup_all_nodes(dbname(Db)). %% some simple type validation and transcoding dbname(DbName) when is_list(DbName) -> @@ -629,7 +605,7 @@ dbname(Db) -> couch_db:name(Db) catch error:badarg -> - erlang:error({illegal_database_name, Db}) + error({illegal_database_name, Db}) end. %% @doc get db shard uuids @@ -648,7 +624,7 @@ docid(DocId) -> docs(Db, Docs) when is_list(Docs) -> [doc(Db, D) || D <- Docs]; docs(_Db, Docs) -> - erlang:error({illegal_docs_list, Docs}). + error({illegal_docs_list, Docs}). doc(_Db, #doc{} = Doc) -> Doc; @@ -658,14 +634,13 @@ doc(Db0, {_} = Doc) -> true -> Db0; false -> - Shard = hd(mem3:shards(Db0)), - Props = couch_util:get_value(props, Shard#shard.opts, []), + Props = mem3:props(Db0), {ok, Db1} = couch_db:clustered_db(Db0, [{props, Props}]), Db1 end, couch_db:doc_from_json_obj_validate(Db, Doc); doc(_Db, Doc) -> - erlang:error({illegal_doc_format, Doc}). + error({illegal_doc_format, Doc}). design_doc(#doc{} = DDoc) -> DDoc; diff --git a/src/fabric/src/fabric_db_create.erl b/src/fabric/src/fabric_db_create.erl index f7c6e54022..3f378dc73e 100644 --- a/src/fabric/src/fabric_db_create.erl +++ b/src/fabric/src/fabric_db_create.erl @@ -158,11 +158,7 @@ make_document([#shard{dbname = DbName} | _] = Shards, Suffix, Options) -> {RawOut, ByNodeOut, ByRangeOut} = lists:foldl( fun(#shard{node = N, range = [B, E]}, {Raw, ByNode, ByRange}) -> - Range = ?l2b([ - couch_util:to_hex(<>), - "-", - couch_util:to_hex(<>) - ]), + Range = mem3_util:range_to_hex([B, E]), Node = couch_util:to_binary(N), { [[<<"add">>, Range, Node] | Raw], @@ -227,7 +223,7 @@ db_exists_for_existing_db() -> db_exists_for_missing_db() -> Mock = fun(DbName) -> - erlang:error(database_does_not_exist, [DbName]) + error(database_does_not_exist, [DbName]) end, meck:expect(mem3, shards, Mock), ?assertEqual(false, db_exists(<<"foobar">>)), diff --git a/src/fabric/src/fabric_db_delete.erl b/src/fabric/src/fabric_db_delete.erl index 6e44c6af54..5a95b03246 100644 --- a/src/fabric/src/fabric_db_delete.erl +++ b/src/fabric/src/fabric_db_delete.erl @@ -29,7 +29,7 @@ go(DbName, _Options) -> {ok, accepted} -> accepted; {ok, not_found} -> - erlang:error(database_does_not_exist, [DbName]); + error(database_does_not_exist, [DbName]); Error -> Error after diff --git a/src/fabric/src/fabric_db_meta.erl b/src/fabric/src/fabric_db_meta.erl index 1013b958d4..af4a069d4c 100644 --- a/src/fabric/src/fabric_db_meta.erl +++ b/src/fabric/src/fabric_db_meta.erl @@ -16,7 +16,8 @@ set_revs_limit/3, set_security/3, get_all_security/2, - set_purge_infos_limit/3 + set_purge_infos_limit/3, + update_props/4 ]). -include_lib("fabric/include/fabric.hrl"). @@ -198,3 +199,25 @@ maybe_finish_get(#acc{workers = []} = Acc) -> {stop, Acc}; maybe_finish_get(Acc) -> {ok, Acc}. + +update_props(DbName, K, V, Options) -> + Shards = mem3:shards(DbName), + Workers = fabric_util:submit_jobs(Shards, update_props, [K, V, Options]), + Handler = fun handle_update_props_message/3, + Acc0 = {Workers, length(Workers) - 1}, + case fabric_util:recv(Workers, #shard.ref, Handler, Acc0) of + {ok, ok} -> + ok; + {timeout, {DefunctWorkers, _}} -> + fabric_util:log_timeout(DefunctWorkers, "update_props"), + {error, timeout}; + Error -> + Error + end. + +handle_update_props_message(ok, _, {_Workers, 0}) -> + {stop, ok}; +handle_update_props_message(ok, Worker, {Workers, Waiting}) -> + {ok, {lists:delete(Worker, Workers), Waiting - 1}}; +handle_update_props_message(Error, _, _Acc) -> + {error, Error}. diff --git a/src/fabric/src/fabric_db_purged_infos.erl b/src/fabric/src/fabric_db_purged_infos.erl index 45f5681b6c..b1c5a606eb 100644 --- a/src/fabric/src/fabric_db_purged_infos.erl +++ b/src/fabric/src/fabric_db_purged_infos.erl @@ -18,8 +18,7 @@ -record(pacc, { counters, - replies, - ring_opts + replies }). go(DbName) -> @@ -29,8 +28,7 @@ go(DbName) -> Fun = fun handle_message/3, Acc0 = #pacc{ counters = fabric_dict:init(Workers, nil), - replies = couch_util:new_set(), - ring_opts = [{any, Shards}] + replies = couch_util:new_set() }, try case fabric_util:recv(Workers, #shard.ref, Fun, Acc0) of @@ -48,17 +46,17 @@ go(DbName) -> end. handle_message({rexi_DOWN, _, {_, NodeRef}, _}, _Shard, #pacc{} = Acc) -> - #pacc{counters = Counters, ring_opts = RingOpts} = Acc, - case fabric_util:remove_down_workers(Counters, NodeRef, RingOpts) of + #pacc{counters = Counters} = Acc, + case fabric_util:remove_down_workers(Counters, NodeRef, [all]) of {ok, NewCounters} -> {ok, Acc#pacc{counters = NewCounters}}; error -> {error, {nodedown, <<"progress not possible">>}} end; handle_message({rexi_EXIT, Reason}, Shard, #pacc{} = Acc) -> - #pacc{counters = Counters, ring_opts = RingOpts} = Acc, + #pacc{counters = Counters} = Acc, NewCounters = fabric_dict:erase(Shard, Counters), - case fabric_ring:is_progress_possible(NewCounters, RingOpts) of + case fabric_ring:is_progress_possible(NewCounters, [all]) of true -> {ok, Acc#pacc{counters = NewCounters}}; false -> @@ -92,8 +90,7 @@ make_shards() -> init_acc(Shards) -> #pacc{ counters = fabric_dict:init(Shards, nil), - replies = couch_util:new_set(), - ring_opts = [{any, Shards}] + replies = couch_util:new_set() }. first_result_ok_test() -> diff --git a/src/fabric/src/fabric_db_update_listener.erl b/src/fabric/src/fabric_db_update_listener.erl index 4f3c30a252..e91f2ec5af 100644 --- a/src/fabric/src/fabric_db_update_listener.erl +++ b/src/fabric/src/fabric_db_update_listener.erl @@ -60,8 +60,8 @@ go(Parent, ParentRef, DbName, Timeout, ClientReq) -> end, case Resp of {ok, _} -> ok; - {error, Error} -> erlang:error(Error); - Error -> erlang:error(Error) + {error, Error} -> error(Error); + Error -> error(Error) end. start_update_notifiers(Shards) -> @@ -99,7 +99,7 @@ handle_db_event(_DbName, _Event, St) -> start_cleanup_monitor(Parent, Notifiers, ClientReq) -> spawn(fun() -> - Ref = erlang:monitor(process, Parent), + Ref = monitor(process, Parent), cleanup_monitor(Parent, Ref, Notifiers, ClientReq) end). @@ -129,11 +129,11 @@ stop({Pid, Ref}) -> erlang:send(Pid, {Ref, done}). wait_db_updated({Pid, Ref}) -> - MonRef = erlang:monitor(process, Pid), + MonRef = monitor(process, Pid), erlang:send(Pid, {Ref, get_state}), receive {state, Pid, State} -> - erlang:demonitor(MonRef, [flush]), + demonitor(MonRef, [flush]), State; {'DOWN', MonRef, process, Pid, _Reason} -> changes_feed_died diff --git a/src/fabric/src/fabric_doc_open.erl b/src/fabric/src/fabric_doc_open.erl index 4946a26bfa..138a3f2bbb 100644 --- a/src/fabric/src/fabric_doc_open.erl +++ b/src/fabric/src/fabric_doc_open.erl @@ -41,11 +41,11 @@ go(DbName, Id, Options) -> ), SuppressDeletedDoc = not lists:member(deleted, Options), N = mem3:n(DbName), - R = couch_util:get_value(r, Options, integer_to_list(mem3:quorum(DbName))), + R = fabric_util:r_from_opts(DbName, Options), Acc0 = #acc{ dbname = DbName, workers = Workers, - r = erlang:min(N, list_to_integer(R)), + r = min(N, R), state = r_not_met, replies = [] }, @@ -317,8 +317,8 @@ t_handle_message_down(_) -> t_handle_message_exit(_) -> Exit = {rexi_EXIT, nil}, - Worker0 = #shard{ref = erlang:make_ref()}, - Worker1 = #shard{ref = erlang:make_ref()}, + Worker0 = #shard{ref = make_ref()}, + Worker1 = #shard{ref = make_ref()}, % Only removes the specified worker ?assertEqual( @@ -338,9 +338,9 @@ t_handle_message_exit(_) -> ). t_handle_message_reply(_) -> - Worker0 = #shard{ref = erlang:make_ref()}, - Worker1 = #shard{ref = erlang:make_ref()}, - Worker2 = #shard{ref = erlang:make_ref()}, + Worker0 = #shard{ref = make_ref()}, + Worker1 = #shard{ref = make_ref()}, + Worker2 = #shard{ref = make_ref()}, Workers = [Worker0, Worker1, Worker2], Acc0 = #acc{workers = Workers, r = 2, replies = []}, @@ -426,9 +426,9 @@ t_handle_message_reply(_) -> ). t_store_node_revs(_) -> - W1 = #shard{node = w1, ref = erlang:make_ref()}, - W2 = #shard{node = w2, ref = erlang:make_ref()}, - W3 = #shard{node = w3, ref = erlang:make_ref()}, + W1 = #shard{node = w1, ref = make_ref()}, + W2 = #shard{node = w2, ref = make_ref()}, + W3 = #shard{node = w3, ref = make_ref()}, Foo1 = {ok, #doc{id = <<"bar">>, revs = {1, [<<"foo">>]}}}, Foo2 = {ok, #doc{id = <<"bar">>, revs = {2, [<<"foo2">>, <<"foo">>]}}}, NFM = {not_found, missing}, diff --git a/src/fabric/src/fabric_doc_open_revs.erl b/src/fabric/src/fabric_doc_open_revs.erl index b0ff994b01..93ec7e71ef 100644 --- a/src/fabric/src/fabric_doc_open_revs.erl +++ b/src/fabric/src/fabric_doc_open_revs.erl @@ -37,12 +37,11 @@ go(DbName, Id, Revs, Options) -> open_revs, [Id, Revs, Options] ), - R = couch_util:get_value(r, Options, integer_to_list(mem3:quorum(DbName))), State = #state{ dbname = DbName, worker_count = length(Workers), workers = Workers, - r = list_to_integer(R), + r = fabric_util:r_from_opts(DbName, Options), revs = Revs, latest = lists:member(latest, Options), replies = [] @@ -213,7 +212,7 @@ maybe_read_repair(Db, IsTree, Replies, NodeRevs, ReplyCount, DoRepair) -> [] -> ok; _ -> - erlang:spawn(fun() -> read_repair(Db, Docs, NodeRevs) end) + spawn(fun() -> read_repair(Db, Docs, NodeRevs) end) end. tree_repair_docs(_Replies, false) -> diff --git a/src/fabric/src/fabric_doc_purge.erl b/src/fabric/src/fabric_doc_purge.erl index 5405ceb600..a4fc9e76fe 100644 --- a/src/fabric/src/fabric_doc_purge.erl +++ b/src/fabric/src/fabric_doc_purge.erl @@ -28,44 +28,41 @@ go(_, [], _) -> {ok, []}; go(DbName, IdsRevs, Options) -> - % Generate our purge requests of {UUID, DocId, Revs} - {UUIDs, Reqs} = create_reqs(IdsRevs, [], []), - - % Fire off rexi workers for each shard. - {Workers, WorkerUUIDs} = dict:fold( - fun(Shard, ShardReqs, {Ws, WUUIDs}) -> + % Generate our purge requests of {UUID, DocId, Revs}. Return: + % * Reqs : [{UUID, DocId, Revs}] + % * UUIDs : [UUID] in the same order as Reqs + % * Responses : #{UUID => []} initial response accumulator + % + {UUIDs, Reqs, Responses} = create_requests_and_responses(IdsRevs), + + % Fire off rexi workers for each shard. Return: + % * Workers : [#shard{ref = Ref}] + % * WorkerUUIDs : #{Worker => [UUID]} + % * UUIDCounts : #{UUID => Counter} + % + {Workers, WorkerUUIDs, UUIDCounts} = maps:fold( + fun(Shard, ShardReqs, {WorkersAcc, WorkersUUIDsAcc, CountsAcc}) -> #shard{name = ShardDbName, node = Node} = Shard, Args = [ShardDbName, ShardReqs, Options], Ref = rexi:cast(Node, {fabric_rpc, purge_docs, Args}), Worker = Shard#shard{ref = Ref}, ShardUUIDs = [UUID || {UUID, _Id, _Revs} <- ShardReqs], - {[Worker | Ws], [{Worker, ShardUUIDs} | WUUIDs]} + Fun = fun(UUID, Acc) -> update_counter(UUID, Acc) end, + CountsAcc1 = lists:foldl(Fun, CountsAcc, ShardUUIDs), + WorkersUUIDAcc1 = WorkersUUIDsAcc#{Worker => ShardUUIDs}, + {[Worker | WorkersAcc], WorkersUUIDAcc1, CountsAcc1} end, - {[], []}, + {[], #{}, #{}}, group_reqs_by_shard(DbName, Reqs) ), - UUIDCounts = lists:foldl( - fun({_Worker, WUUIDs}, CountAcc) -> - lists:foldl( - fun(UUID, InnerCountAcc) -> - dict:update_counter(UUID, 1, InnerCountAcc) - end, - CountAcc, - WUUIDs - ) - end, - dict:new(), - WorkerUUIDs - ), - RexiMon = fabric_util:create_monitors(Workers), Timeout = fabric_util:request_timeout(), Acc0 = #acc{ worker_uuids = WorkerUUIDs, - resps = dict:from_list([{UUID, []} || UUID <- UUIDs]), + resps = Responses, uuid_counts = UUIDCounts, - w = w(DbName, Options) + w = fabric_util:w_from_opts(DbName, Options) }, Callback = fun handle_message/3, Acc2 = @@ -85,8 +82,9 @@ handle_message({rexi_DOWN, _, {_, Node}, _}, _Worker, Acc) -> worker_uuids = WorkerUUIDs, resps = Resps } = Acc, - Pred = fun({#shard{node = N}, _}) -> N == Node end, - {Failed, Rest} = lists:partition(Pred, WorkerUUIDs), + Pred = fun(#shard{node = N}, _) -> N == Node end, + Failed = maps:filter(Pred, WorkerUUIDs), + Rest = maps:without(maps:keys(Failed), WorkerUUIDs), NewResps = append_errors(internal_server_error, Failed, Resps), maybe_stop(Acc#acc{worker_uuids = Rest, resps = NewResps}); handle_message({rexi_EXIT, _}, Worker, Acc) -> @@ -94,60 +92,47 @@ handle_message({rexi_EXIT, _}, Worker, Acc) -> worker_uuids = WorkerUUIDs, resps = Resps } = Acc, - {value, WorkerPair, Rest} = lists:keytake(Worker, 1, WorkerUUIDs), - NewResps = append_errors(internal_server_error, [WorkerPair], Resps), - maybe_stop(Acc#acc{worker_uuids = Rest, resps = NewResps}); + {FailedUUIDs, WorkerUUIDs1} = maps:take(Worker, WorkerUUIDs), + NewResps = append_errors(internal_server_error, #{Worker => FailedUUIDs}, Resps), + maybe_stop(Acc#acc{worker_uuids = WorkerUUIDs1, resps = NewResps}); handle_message({ok, Replies}, Worker, Acc) -> #acc{ worker_uuids = WorkerUUIDs, resps = Resps } = Acc, - {value, {_W, UUIDs}, Rest} = lists:keytake(Worker, 1, WorkerUUIDs), + {UUIDs, WorkerUUIDs1} = maps:take(Worker, WorkerUUIDs), NewResps = append_resps(UUIDs, Replies, Resps), - maybe_stop(Acc#acc{worker_uuids = Rest, resps = NewResps}); + maybe_stop(Acc#acc{worker_uuids = WorkerUUIDs1, resps = NewResps}); handle_message({bad_request, Msg}, _, _) -> throw({bad_request, Msg}). handle_timeout(#acc{worker_uuids = DefunctWorkerUUIDs, resps = Resps} = Acc) -> - DefunctWorkers = [Worker || {Worker, _} <- DefunctWorkerUUIDs], + DefunctWorkers = maps:keys(DefunctWorkerUUIDs), fabric_util:log_timeout(DefunctWorkers, "purge_docs"), NewResps = append_errors(timeout, DefunctWorkerUUIDs, Resps), - Acc#acc{worker_uuids = [], resps = NewResps}. + Acc#acc{worker_uuids = #{}, resps = NewResps}. -create_reqs([], UUIDs, Reqs) -> - {lists:reverse(UUIDs), lists:reverse(Reqs)}; -create_reqs([{Id, Revs} | RestIdsRevs], UUIDs, Reqs) -> - UUID = couch_uuids:new(), - NewUUIDs = [UUID | UUIDs], - NewReqs = [{UUID, Id, lists:usort(Revs)} | Reqs], - create_reqs(RestIdsRevs, NewUUIDs, NewReqs). +create_requests_and_responses(IdsRevs) -> + Fun = fun({Id, Revs}, {UUIDsAcc, RespAcc}) -> + UUID = couch_uuids:v7_bin(), + {{UUID, Id, lists:usort(Revs)}, {[UUID | UUIDsAcc], RespAcc#{UUID => []}}} + end, + {IdRevs1, {UUIDs, Resps}} = lists:mapfoldl(Fun, {[], #{}}, IdsRevs), + {lists:reverse(UUIDs), IdRevs1, Resps}. group_reqs_by_shard(DbName, Reqs) -> - lists:foldl( - fun({_UUID, Id, _Revs} = Req, D0) -> - lists:foldl( - fun(Shard, D1) -> - dict:append(Shard, Req, D1) - end, - D0, - mem3:shards(DbName, Id) - ) + ReqFoldFun = + fun({_UUID, Id, _Revs} = Req, #{} = Map0) -> + AppendFun = fun(Shard, Map1) -> map_append(Shard, Req, Map1) end, + lists:foldl(AppendFun, Map0, mem3:shards(DbName, Id)) end, - dict:new(), - Reqs - ). + lists:foldl(ReqFoldFun, #{}, Reqs). -w(DbName, Options) -> - try - list_to_integer(couch_util:get_value(w, Options)) - catch - _:_ -> - mem3:quorum(DbName) - end. - -append_errors(Type, WorkerUUIDs, Resps) -> - lists:foldl( - fun({_Worker, UUIDs}, RespAcc) -> +% Failed WorkerUUIDs = #{#shard{} => [UUIDs, ...]} +% Resps = #{UUID => [{ok, ...} | {error, ...}] +append_errors(Type, #{} = WorkerUUIDs, #{} = Resps) -> + maps:fold( + fun(_Worker, UUIDs, RespAcc) -> Errors = [{error, Type} || _UUID <- UUIDs], append_resps(UUIDs, Errors, RespAcc) end, @@ -155,59 +140,54 @@ append_errors(Type, WorkerUUIDs, Resps) -> WorkerUUIDs ). -append_resps([], [], Resps) -> +append_resps([], [], #{} = Resps) -> Resps; -append_resps([UUID | RestUUIDs], [Reply | RestReplies], Resps) -> - NewResps = dict:append(UUID, Reply, Resps), +append_resps([UUID | RestUUIDs], [Reply | RestReplies], #{} = Resps) -> + NewResps = map_append(UUID, Reply, Resps), append_resps(RestUUIDs, RestReplies, NewResps). -maybe_stop(#acc{worker_uuids = []} = Acc) -> +maybe_stop(#acc{worker_uuids = #{} = Map} = Acc) when map_size(Map) == 0 -> {stop, Acc}; -maybe_stop(#acc{resps = Resps, uuid_counts = Counts, w = W} = Acc) -> +maybe_stop(#acc{resps = #{} = Resps, uuid_counts = #{} = Counts, w = W} = Acc) -> try - dict:fold( - fun(UUID, UUIDResps, _) -> - UUIDCount = dict:fetch(UUID, Counts), + Fun = + fun(UUID, UUIDResps) -> + #{UUID := UUIDCount} = Counts, case has_quorum(UUIDResps, UUIDCount, W) of true -> ok; false -> throw(keep_going) end end, - nil, - Resps - ), + maps:foreach(Fun, Resps), {stop, Acc} catch throw:keep_going -> {ok, Acc} end. -format_resps(UUIDs, #acc{} = Acc) -> - #acc{ - resps = Resps, - w = W - } = Acc, - FoldFun = fun(UUID, Replies, ReplyAcc) -> +format_resps(UUIDs, #acc{resps = Resps, w = W}) -> + Fun = fun(_UUID, Replies) -> OkReplies = [Reply || {ok, Reply} <- Replies], case OkReplies of [] -> [Error | _] = lists:usort(Replies), - [{UUID, Error} | ReplyAcc]; - _ -> + Error; + [_ | _] -> AllRevs = lists:usort(lists:flatten(OkReplies)), - IsOk = - length(OkReplies) >= W andalso - length(lists:usort(OkReplies)) == 1, + IsOk = length(OkReplies) >= W andalso length(lists:usort(OkReplies)) == 1, Health = if IsOk -> ok; true -> accepted end, - [{UUID, {Health, AllRevs}} | ReplyAcc] + {Health, AllRevs} end end, - FinalReplies = dict:fold(FoldFun, [], Resps), - couch_util:reorder_results(UUIDs, FinalReplies); + FinalReplies = maps:map(Fun, Resps), + % Reorder results in the same order as the the initial IdRevs + % this also implicitly asserts that the all UUIDs should have + % a matching reply + [map_get(UUID, FinalReplies) || UUID <- UUIDs]; format_resps(_UUIDs, Else) -> Else. @@ -225,22 +205,45 @@ resp_health(Resps) -> has_quorum(Resps, Count, W) -> OkResps = [R || {ok, _} = R <- Resps], - OkCounts = lists:foldl( - fun(R, Acc) -> - orddict:update_counter(R, 1, Acc) - end, - orddict:new(), - OkResps - ), - MaxOk = lists:max([0 | element(2, lists:unzip(OkCounts))]), + OkCounts = lists:foldl(fun(R, Acc) -> update_counter(R, Acc) end, #{}, OkResps), + MaxOk = lists:max([0 | maps:values(OkCounts)]), if MaxOk >= W -> true; length(Resps) >= Count -> true; true -> false end. +map_append(Key, Val, #{} = Map) -> + maps:update_with(Key, fun(V) -> [Val | V] end, [Val], Map). + +update_counter(Key, #{} = Map) -> + maps:update_with(Key, fun(V) -> V + 1 end, 1, Map). + -ifdef(TEST). + -include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). + +response_health_test() -> + ?assertEqual(error, resp_health([])), + ?assertEqual(error, resp_health([{potato, x}])), + ?assertEqual(ok, resp_health([{ok, x}, {ok, y}])), + ?assertEqual(accepted, resp_health([{accepted, x}])), + ?assertEqual(accepted, resp_health([{ok, x}, {accepted, y}])), + ?assertEqual(error, resp_health([{error, x}])), + ?assertEqual(error, resp_health([{ok, x}, {error, y}])), + ?assertEqual(error, resp_health([{error, x}, {accepted, y}, {ok, z}])). + +has_quorum_test() -> + ?assertEqual(true, has_quorum([], 0, 0)), + ?assertEqual(true, has_quorum([], 1, 0)), + ?assertEqual(true, has_quorum([], 0, 1)), + ?assertEqual(false, has_quorum([], 1, 1)), + ?assertEqual(true, has_quorum([{ok, x}], 1, 1)), + ?assertEqual(true, has_quorum([{accepted, x}], 1, 1)), + ?assertEqual(false, has_quorum([{accepted, x}], 2, 1)), + ?assertEqual(false, has_quorum([{accepted, x}, {ok, y}], 3, 2)), + ?assertEqual(true, has_quorum([{accepted, x}, {ok, y}], 2, 2)). purge_test_() -> { @@ -248,6 +251,8 @@ purge_test_() -> fun setup/0, fun teardown/1, with([ + ?TDEF(t_create_reqs), + ?TDEF(t_w2_ok), ?TDEF(t_w3_ok), @@ -262,29 +267,53 @@ purge_test_() -> ?TDEF(t_mixed_ok_accepted), ?TDEF(t_mixed_errors), + ?TDEF(t_rexi_down_error), ?TDEF(t_timeout) ]) }. setup() -> - meck:new(couch_log), - meck:expect(couch_log, warning, fun(_, _) -> ok end), - meck:expect(couch_log, notice, fun(_, _) -> ok end), - meck:expect(couch_log, error, fun(_, _) -> ok end). + test_util:start_couch(). -teardown(_) -> - meck:unload(). +teardown(Ctx) -> + test_util:stop_couch(Ctx). + +t_create_reqs(_) -> + ?assertEqual({[], [], #{}}, create_requests_and_responses([])), + IdRevs = [ + {<<"3">>, []}, + {<<"1">>, [<<"2-b">>, <<"1-a">>]}, + {<<"2">>, [<<"3-c">>, <<"1-d">>, <<"3-c">>]} + ], + Res = create_requests_and_responses(IdRevs), + ?assertMatch({[<<_/binary>> | _], [{<<_/binary>>, _, _} | _], #{}}, Res), + {UUIDs, IdRevs1, Resps} = Res, + ?assertEqual(3, length(UUIDs)), + ?assertEqual(3, length(IdRevs1)), + ?assertEqual(3, map_size(Resps)), + ?assertEqual(lists:sort(UUIDs), lists:sort(maps:keys(Resps))), + {IdRevsUUIDs, DocIds, Revs} = lists:unzip3(IdRevs1), + ?assertEqual(UUIDs, IdRevsUUIDs), + ?assertEqual([<<"3">>, <<"1">>, <<"2">>], DocIds), + ?assertEqual( + [ + [], + [<<"1-a">>, <<"2-b">>], + [<<"1-d">>, <<"3-c">>] + ], + Revs + ). t_w2_ok(_) -> Acc0 = create_init_acc(2), Msg = {ok, [{ok, [{1, <<"foo">>}]}, {ok, [{2, <<"bar">>}]}]}, {ok, Acc1} = handle_message(Msg, worker(1, Acc0), Acc0), - ?assertEqual(2, length(Acc1#acc.worker_uuids)), + ?assertEqual(2, map_size(Acc1#acc.worker_uuids)), check_quorum(Acc1, false), {stop, Acc2} = handle_message(Msg, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, true), Expect = [{ok, [{1, <<"foo">>}]}, {ok, [{2, <<"bar">>}]}], @@ -300,11 +329,11 @@ t_w3_ok(_) -> check_quorum(Acc1, false), {ok, Acc2} = handle_message(Msg, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, false), {stop, Acc3} = handle_message(Msg, worker(3, Acc0), Acc2), - ?assertEqual(0, length(Acc3#acc.worker_uuids)), + ?assertEqual(0, map_size(Acc3#acc.worker_uuids)), check_quorum(Acc3, true), Expect = [{ok, [{1, <<"foo">>}]}, {ok, [{2, <<"bar">>}]}], @@ -318,15 +347,15 @@ t_w2_mixed_accepted(_) -> Msg2 = {ok, [{ok, [{1, <<"foo2">>}]}, {ok, [{2, <<"bar2">>}]}]}, {ok, Acc1} = handle_message(Msg1, worker(1, Acc0), Acc0), - ?assertEqual(2, length(Acc1#acc.worker_uuids)), + ?assertEqual(2, map_size(Acc1#acc.worker_uuids)), check_quorum(Acc1, false), {ok, Acc2} = handle_message(Msg2, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, false), {stop, Acc3} = handle_message(Msg1, worker(3, Acc0), Acc2), - ?assertEqual(0, length(Acc3#acc.worker_uuids)), + ?assertEqual(0, map_size(Acc3#acc.worker_uuids)), check_quorum(Acc3, true), Expect = [ @@ -343,15 +372,15 @@ t_w3_mixed_accepted(_) -> Msg2 = {ok, [{ok, [{1, <<"foo2">>}]}, {ok, [{2, <<"bar2">>}]}]}, {ok, Acc1} = handle_message(Msg1, worker(1, Acc0), Acc0), - ?assertEqual(2, length(Acc1#acc.worker_uuids)), + ?assertEqual(2, map_size(Acc1#acc.worker_uuids)), check_quorum(Acc1, false), {ok, Acc2} = handle_message(Msg2, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, false), {stop, Acc3} = handle_message(Msg2, worker(3, Acc0), Acc2), - ?assertEqual(0, length(Acc3#acc.worker_uuids)), + ?assertEqual(0, map_size(Acc3#acc.worker_uuids)), check_quorum(Acc3, true), Expect = [ @@ -368,15 +397,15 @@ t_w2_exit1_ok(_) -> ExitMsg = {rexi_EXIT, blargh}, {ok, Acc1} = handle_message(Msg, worker(1, Acc0), Acc0), - ?assertEqual(2, length(Acc1#acc.worker_uuids)), + ?assertEqual(2, map_size(Acc1#acc.worker_uuids)), check_quorum(Acc1, false), {ok, Acc2} = handle_message(ExitMsg, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, false), {stop, Acc3} = handle_message(Msg, worker(3, Acc0), Acc2), - ?assertEqual(0, length(Acc3#acc.worker_uuids)), + ?assertEqual(0, map_size(Acc3#acc.worker_uuids)), check_quorum(Acc3, true), Expect = [{ok, [{1, <<"foo">>}]}, {ok, [{2, <<"bar">>}]}], @@ -390,15 +419,15 @@ t_w2_exit2_accepted(_) -> ExitMsg = {rexi_EXIT, blargh}, {ok, Acc1} = handle_message(Msg, worker(1, Acc0), Acc0), - ?assertEqual(2, length(Acc1#acc.worker_uuids)), + ?assertEqual(2, map_size(Acc1#acc.worker_uuids)), check_quorum(Acc1, false), {ok, Acc2} = handle_message(ExitMsg, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, false), {stop, Acc3} = handle_message(ExitMsg, worker(3, Acc0), Acc2), - ?assertEqual(0, length(Acc3#acc.worker_uuids)), + ?assertEqual(0, map_size(Acc3#acc.worker_uuids)), check_quorum(Acc3, true), Expect = [{accepted, [{1, <<"foo">>}]}, {accepted, [{2, <<"bar">>}]}], @@ -411,15 +440,15 @@ t_w2_exit3_error(_) -> ExitMsg = {rexi_EXIT, blargh}, {ok, Acc1} = handle_message(ExitMsg, worker(1, Acc0), Acc0), - ?assertEqual(2, length(Acc1#acc.worker_uuids)), + ?assertEqual(2, map_size(Acc1#acc.worker_uuids)), check_quorum(Acc1, false), {ok, Acc2} = handle_message(ExitMsg, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, false), {stop, Acc3} = handle_message(ExitMsg, worker(3, Acc0), Acc2), - ?assertEqual(0, length(Acc3#acc.worker_uuids)), + ?assertEqual(0, map_size(Acc3#acc.worker_uuids)), check_quorum(Acc3, true), Expect = [ @@ -439,15 +468,15 @@ t_w4_accepted(_) -> Msg = {ok, [{ok, [{1, <<"foo">>}]}, {ok, [{2, <<"bar">>}]}]}, {ok, Acc1} = handle_message(Msg, worker(1, Acc0), Acc0), - ?assertEqual(2, length(Acc1#acc.worker_uuids)), + ?assertEqual(2, map_size(Acc1#acc.worker_uuids)), check_quorum(Acc1, false), {ok, Acc2} = handle_message(Msg, worker(2, Acc0), Acc1), - ?assertEqual(1, length(Acc2#acc.worker_uuids)), + ?assertEqual(1, map_size(Acc2#acc.worker_uuids)), check_quorum(Acc2, false), {stop, Acc3} = handle_message(Msg, worker(3, Acc0), Acc2), - ?assertEqual(0, length(Acc3#acc.worker_uuids)), + ?assertEqual(0, map_size(Acc3#acc.worker_uuids)), check_quorum(Acc3, true), Expect = [{accepted, [{1, <<"foo">>}]}, {accepted, [{2, <<"bar">>}]}], @@ -456,20 +485,20 @@ t_w4_accepted(_) -> ?assertEqual(accepted, resp_health(Resps)). t_mixed_ok_accepted(_) -> - WorkerUUIDs = [ - {#shard{node = a, range = [1, 2]}, [<<"uuid1">>]}, - {#shard{node = b, range = [1, 2]}, [<<"uuid1">>]}, - {#shard{node = c, range = [1, 2]}, [<<"uuid1">>]}, - - {#shard{node = a, range = [3, 4]}, [<<"uuid2">>]}, - {#shard{node = b, range = [3, 4]}, [<<"uuid2">>]}, - {#shard{node = c, range = [3, 4]}, [<<"uuid2">>]} - ], + WorkerUUIDs = #{ + #shard{node = a, range = [1, 2]} => [<<"uuid1">>], + #shard{node = b, range = [1, 2]} => [<<"uuid1">>], + #shard{node = c, range = [1, 2]} => [<<"uuid1">>], + + #shard{node = a, range = [3, 4]} => [<<"uuid2">>], + #shard{node = b, range = [3, 4]} => [<<"uuid2">>], + #shard{node = c, range = [3, 4]} => [<<"uuid2">>] + }, Acc0 = #acc{ worker_uuids = WorkerUUIDs, - resps = dict:from_list([{<<"uuid1">>, []}, {<<"uuid2">>, []}]), - uuid_counts = dict:from_list([{<<"uuid1">>, 3}, {<<"uuid2">>, 3}]), + resps = maps:from_list([{<<"uuid1">>, []}, {<<"uuid2">>, []}]), + uuid_counts = maps:from_list([{<<"uuid1">>, 3}, {<<"uuid2">>, 3}]), w = 2 }, @@ -477,11 +506,11 @@ t_mixed_ok_accepted(_) -> Msg2 = {ok, [{ok, [{2, <<"bar">>}]}]}, ExitMsg = {rexi_EXIT, blargh}, - {ok, Acc1} = handle_message(Msg1, worker(1, Acc0), Acc0), - {ok, Acc2} = handle_message(Msg1, worker(2, Acc0), Acc1), - {ok, Acc3} = handle_message(ExitMsg, worker(4, Acc0), Acc2), - {ok, Acc4} = handle_message(ExitMsg, worker(5, Acc0), Acc3), - {stop, Acc5} = handle_message(Msg2, worker(6, Acc0), Acc4), + {ok, Acc1} = handle_message(Msg1, worker(a, [1, 2], Acc0), Acc0), + {ok, Acc2} = handle_message(Msg1, worker(b, [1, 2], Acc0), Acc1), + {ok, Acc3} = handle_message(ExitMsg, worker(a, [3, 4], Acc0), Acc2), + {ok, Acc4} = handle_message(ExitMsg, worker(b, [3, 4], Acc0), Acc3), + {stop, Acc5} = handle_message(Msg2, worker(c, [3, 4], Acc0), Acc4), Expect = [{ok, [{1, <<"foo">>}]}, {accepted, [{2, <<"bar">>}]}], Resps = format_resps([<<"uuid1">>, <<"uuid2">>], Acc5), @@ -489,59 +518,94 @@ t_mixed_ok_accepted(_) -> ?assertEqual(accepted, resp_health(Resps)). t_mixed_errors(_) -> - WorkerUUIDs = [ - {#shard{node = a, range = [1, 2]}, [<<"uuid1">>]}, - {#shard{node = b, range = [1, 2]}, [<<"uuid1">>]}, - {#shard{node = c, range = [1, 2]}, [<<"uuid1">>]}, - - {#shard{node = a, range = [3, 4]}, [<<"uuid2">>]}, - {#shard{node = b, range = [3, 4]}, [<<"uuid2">>]}, - {#shard{node = c, range = [3, 4]}, [<<"uuid2">>]} - ], + WorkerUUIDs = #{ + #shard{node = a, range = [1, 2]} => [<<"uuid1">>], + #shard{node = b, range = [1, 2]} => [<<"uuid1">>], + #shard{node = c, range = [1, 2]} => [<<"uuid1">>], + + #shard{node = a, range = [3, 4]} => [<<"uuid2">>], + #shard{node = b, range = [3, 4]} => [<<"uuid2">>], + #shard{node = c, range = [3, 4]} => [<<"uuid2">>] + }, Acc0 = #acc{ worker_uuids = WorkerUUIDs, - resps = dict:from_list([{<<"uuid1">>, []}, {<<"uuid2">>, []}]), - uuid_counts = dict:from_list([{<<"uuid1">>, 3}, {<<"uuid2">>, 3}]), + resps = maps:from_list([{<<"uuid1">>, []}, {<<"uuid2">>, []}]), + uuid_counts = maps:from_list([{<<"uuid1">>, 3}, {<<"uuid2">>, 3}]), w = 2 }, Msg = {ok, [{ok, [{1, <<"foo">>}]}]}, ExitMsg = {rexi_EXIT, blargh}, - {ok, Acc1} = handle_message(Msg, worker(1, Acc0), Acc0), - {ok, Acc2} = handle_message(Msg, worker(2, Acc0), Acc1), - {ok, Acc3} = handle_message(ExitMsg, worker(4, Acc0), Acc2), - {ok, Acc4} = handle_message(ExitMsg, worker(5, Acc0), Acc3), - {stop, Acc5} = handle_message(ExitMsg, worker(6, Acc0), Acc4), + {ok, Acc1} = handle_message(Msg, worker(a, [1, 2], Acc0), Acc0), + {ok, Acc2} = handle_message(Msg, worker(b, [1, 2], Acc0), Acc1), + {ok, Acc3} = handle_message(ExitMsg, worker(a, [3, 4], Acc0), Acc2), + {ok, Acc4} = handle_message(ExitMsg, worker(b, [3, 4], Acc0), Acc3), + {stop, Acc5} = handle_message(ExitMsg, worker(c, [3, 4], Acc0), Acc4), Expect = [{ok, [{1, <<"foo">>}]}, {error, internal_server_error}], Resps = format_resps([<<"uuid1">>, <<"uuid2">>], Acc5), ?assertEqual(Expect, Resps), ?assertEqual(error, resp_health(Resps)). -t_timeout(_) -> - WorkerUUIDs = [ - {#shard{node = a, range = [1, 2]}, [<<"uuid1">>]}, - {#shard{node = b, range = [1, 2]}, [<<"uuid1">>]}, - {#shard{node = c, range = [1, 2]}, [<<"uuid1">>]}, - - {#shard{node = a, range = [3, 4]}, [<<"uuid2">>]}, - {#shard{node = b, range = [3, 4]}, [<<"uuid2">>]}, - {#shard{node = c, range = [3, 4]}, [<<"uuid2">>]} +t_rexi_down_error(_) -> + WorkerUUIDs = #{ + #shard{node = a, range = [1, 2]} => [<<"uuid1">>], + #shard{node = b, range = [1, 2]} => [<<"uuid1">>], + #shard{node = c, range = [1, 2]} => [<<"uuid1">>], + + #shard{node = a, range = [3, 4]} => [<<"uuid2">>], + #shard{node = b, range = [3, 4]} => [<<"uuid2">>], + #shard{node = c, range = [3, 4]} => [<<"uuid2">>] + }, + + Acc0 = #acc{ + worker_uuids = WorkerUUIDs, + resps = maps:from_list([{<<"uuid1">>, []}, {<<"uuid2">>, []}]), + uuid_counts = maps:from_list([{<<"uuid1">>, 3}, {<<"uuid2">>, 3}]), + w = 2 + }, + + Msg = {ok, [{ok, [{1, <<"foo">>}]}]}, + {ok, Acc1} = handle_message(Msg, worker(a, [1, 2], Acc0), Acc0), + + DownMsgB = {rexi_DOWN, nodedown, {nil, b}, nil}, + {ok, Acc2} = handle_message(DownMsgB, worker(b, [1, 2], Acc0), Acc1), + + DownMsgC = {rexi_DOWN, nodedown, {nil, c}, nil}, + {ok, Acc3} = handle_message(DownMsgC, worker(c, [3, 4], Acc0), Acc2), + + Expect = [ + {accepted, [{1, <<"foo">>}]}, + {error, internal_server_error} ], + Resps = format_resps([<<"uuid1">>, <<"uuid2">>], Acc3), + ?assertEqual(Expect, Resps), + ?assertEqual(error, resp_health(Resps)). + +t_timeout(_) -> + WorkerUUIDs = #{ + #shard{node = a, range = [1, 2]} => [<<"uuid1">>], + #shard{node = b, range = [1, 2]} => [<<"uuid1">>], + #shard{node = c, range = [1, 2]} => [<<"uuid1">>], + + #shard{node = a, range = [3, 4]} => [<<"uuid2">>], + #shard{node = b, range = [3, 4]} => [<<"uuid2">>], + #shard{node = c, range = [3, 4]} => [<<"uuid2">>] + }, Acc0 = #acc{ worker_uuids = WorkerUUIDs, - resps = dict:from_list([{<<"uuid1">>, []}, {<<"uuid2">>, []}]), - uuid_counts = dict:from_list([{<<"uuid1">>, 3}, {<<"uuid2">>, 3}]), + resps = maps:from_list([{<<"uuid1">>, []}, {<<"uuid2">>, []}]), + uuid_counts = maps:from_list([{<<"uuid1">>, 3}, {<<"uuid2">>, 3}]), w = 2 }, Msg = {ok, [{ok, [{1, <<"foo">>}]}]}, - {ok, Acc1} = handle_message(Msg, worker(1, Acc0), Acc0), - {ok, Acc2} = handle_message(Msg, worker(2, Acc0), Acc1), - {ok, Acc3} = handle_message(Msg, worker(3, Acc0), Acc2), + {ok, Acc1} = handle_message(Msg, worker(a, [1, 2], Acc0), Acc0), + {ok, Acc2} = handle_message(Msg, worker(b, [1, 2], Acc0), Acc1), + {ok, Acc3} = handle_message(Msg, worker(c, [1, 2], Acc0), Acc2), Acc4 = handle_timeout(Acc3), Resps = format_resps([<<"uuid1">>, <<"uuid2">>], Acc4), ?assertEqual([{ok, [{1, <<"foo">>}]}, {error, timeout}], Resps). @@ -556,31 +620,124 @@ create_init_acc(W) -> % Create our worker_uuids. We're relying on the fact that % we're using a fake Q=1 db so we don't have to worry % about any hashing here. - WorkerUUIDs = lists:map( - fun(Shard) -> - {Shard#shard{ref = erlang:make_ref()}, [UUID1, UUID2]} - end, - Shards - ), + UUIDs = [UUID1, UUID2], + Workers = [{S#shard{ref = make_ref()}, UUIDs} || S <- Shards], + WorkerUUIDs = maps:from_list(Workers), #acc{ worker_uuids = WorkerUUIDs, - resps = dict:from_list([{UUID1, []}, {UUID2, []}]), - uuid_counts = dict:from_list([{UUID1, 3}, {UUID2, 3}]), + resps = #{UUID1 => [], UUID2 => []}, + uuid_counts = #{UUID1 => 3, UUID2 => 3}, w = W }. +worker(Node, Range, #acc{worker_uuids = WorkerUUIDs}) -> + Workers = maps:keys(WorkerUUIDs), + Pred = fun(#shard{node = N, range = R}) -> + Node =:= N andalso Range =:= R + end, + case lists:filter(Pred, Workers) of + [W] -> W; + _ -> error(not_found) + end. + worker(N, #acc{worker_uuids = WorkerUUIDs}) -> - {Worker, _} = lists:nth(N, WorkerUUIDs), - Worker. + Workers = maps:keys(WorkerUUIDs), + lists:nth(N, lists:sort(Workers)). check_quorum(Acc, Expect) -> - dict:fold( - fun(_Shard, Resps, _) -> + maps:map( + fun(_Shard, Resps) -> ?assertEqual(Expect, has_quorum(Resps, 3, Acc#acc.w)) end, - nil, Acc#acc.resps ). +purge_end_to_end_test_() -> + { + setup, + fun() -> + Ctx = test_util:start_couch([fabric]), + DbName = ?tempdb(), + ok = fabric:create_db(DbName, [{q, 2}, {n, 1}]), + {Ctx, DbName} + end, + fun({Ctx, DbName}) -> + fabric:delete_db(DbName), + test_util:stop_couch(Ctx), + meck:unload() + end, + with([ + ?TDEF(t_purge), + ?TDEF(t_purge_missing_doc_id), + ?TDEF(t_purge_missing_rev) + ]) + }. + +t_purge({_Ctx, DbName}) -> + Rev1 = update_doc(DbName, <<"1">>), + Rev2 = update_doc(DbName, <<"2">>), + Rev3 = update_doc(DbName, <<"3">>), + Res = fabric:purge_docs( + DbName, + [ + {<<"3">>, [Rev3]}, + {<<"1">>, [Rev1]}, + {<<"2">>, [Rev2]} + ], + [] + ), + ?assertMatch({ok, [_, _, _]}, Res), + {ok, [Res3, Res1, Res2]} = Res, + ?assertMatch({ok, [Rev1]}, Res1), + ?assertMatch({ok, [Rev2]}, Res2), + ?assertMatch({ok, [Rev3]}, Res3). + +t_purge_missing_doc_id({_Ctx, DbName}) -> + ?assertMatch({ok, []}, fabric:purge_docs(DbName, [], [])), + Rev1 = update_doc(DbName, <<"1">>), + Rev2 = update_doc(DbName, <<"2">>), + Res = fabric:purge_docs( + DbName, + [ + {<<"3">>, [Rev1]}, + {<<"1">>, [Rev1]}, + {<<"2">>, [Rev2]} + ], + [] + ), + ?assertMatch({ok, [_, _, _]}, Res), + {ok, [Res3, Res1, Res2]} = Res, + ?assertMatch({ok, [Rev1]}, Res1), + ?assertMatch({ok, [Rev2]}, Res2), + ?assertMatch({ok, []}, Res3). + +t_purge_missing_rev({_Ctx, DbName}) -> + Rev1 = update_doc(DbName, <<"1">>), + Rev2 = update_doc(DbName, <<"2">>), + update_doc(DbName, <<"3">>), + Res = fabric:purge_docs( + DbName, + [ + {<<"1">>, [Rev2, Rev1]}, + {<<"2">>, [Rev1]}, + {<<"3">>, []} + ], + [] + ), + ?assertMatch({ok, [_, _, _]}, Res), + {ok, [Res1, Res2, Res3]} = Res, + ?assertMatch({ok, [Rev1]}, Res1), + ?assertMatch({ok, []}, Res2), + ?assertMatch({ok, []}, Res3). + +update_doc(DbName, Id) -> + fabric_util:isolate(fun() -> + Data = binary:encode_hex(crypto:strong_rand_bytes(10)), + Doc = #doc{id = Id, body = {[{<<"foo">>, Data}]}}, + case fabric:update_doc(DbName, Doc, []) of + {ok, Res} -> Res + end + end). + -endif. diff --git a/src/fabric/src/fabric_doc_update.erl b/src/fabric/src/fabric_doc_update.erl index 1f5755de09..a977180bc4 100644 --- a/src/fabric/src/fabric_doc_update.erl +++ b/src/fabric/src/fabric_doc_update.erl @@ -42,11 +42,10 @@ go(DbName, AllDocs0, Opts) -> ), {Workers, _} = lists:unzip(GroupedDocs), RexiMon = fabric_util:create_monitors(Workers), - W = couch_util:get_value(w, Options, integer_to_list(mem3:quorum(DbName))), Acc0 = #acc{ waiting_count = length(Workers), doc_count = length(AllDocs), - w = list_to_integer(W), + w = fabric_util:w_from_opts(DbName, Options), grouped_docs = GroupedDocs, reply = dict:new() }, diff --git a/src/fabric/src/fabric_index_cleanup.erl b/src/fabric/src/fabric_index_cleanup.erl new file mode 100644 index 0000000000..13759ba1d1 --- /dev/null +++ b/src/fabric/src/fabric_index_cleanup.erl @@ -0,0 +1,81 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(fabric_index_cleanup). + +-export([ + cleanup_all_nodes/0, + cleanup_all_nodes/1, + cleanup_this_node/0, + cleanup_this_node/1 +]). + +cleanup_all_nodes() -> + Fun = fun(DbName, _) -> cleanup_all_nodes(DbName) end, + mem3:fold_dbs(Fun, nil), + ok. + +cleanup_all_nodes(DbName) -> + cleanup_indexes(DbName, mem3_util:live_nodes()). + +cleanup_this_node() -> + Fun = fun(DbName, _) -> + case mem3:local_shards(DbName) of + [_ | _] -> cleanup_this_node(DbName); + [] -> ok + end + end, + mem3:fold_dbs(Fun, nil), + ok. + +cleanup_this_node(DbName) -> + cleanup_indexes(DbName, [config:node_name()]). + +cleanup_indexes(DbName, Nodes) -> + try fabric_util:get_design_doc_records(DbName) of + {ok, DDocs} when is_list(DDocs) -> + VSigs = couch_mrview_util:get_signatures_from_ddocs(DbName, DDocs), + DSigs = dreyfus_util:get_signatures_from_ddocs(DbName, DDocs), + NSigs = nouveau_util:get_signatures_from_ddocs(DbName, DDocs), + Shards = [S || S <- mem3:shards(DbName), lists:member(mem3:node(S), Nodes)], + ByNode = maps:groups_from_list(fun mem3:node/1, fun mem3:name/1, Shards), + Fun = fun(Node, Dbs, Acc) -> + Acc1 = send(Node, couch_mrview_cleanup, cleanup, [Dbs, VSigs], Acc), + Acc2 = send(Node, dreyfus_fabric_cleanup, go_local, [DbName, Dbs, DSigs], Acc1), + Acc3 = send(Node, nouveau_fabric_cleanup, go_local, [DbName, Dbs, NSigs], Acc2), + Acc3 + end, + Reqs = maps:fold(Fun, erpc:reqids_new(), ByNode), + recv(DbName, Reqs, fabric_util:abs_request_timeout()); + Error -> + couch_log:error("~p : error fetching ddocs db:~p ~p", [?MODULE, DbName, Error]), + Error + catch + error:database_does_not_exist -> + ok + end. + +send(Node, M, F, A, Reqs) -> + Label = {Node, M, F}, + erpc:send_request(Node, M, F, A, Label, Reqs). + +recv(DbName, Reqs, Timeout) -> + case erpc:receive_response(Reqs, Timeout, true) of + {ok, _Label, Reqs1} -> + recv(DbName, Reqs1, Timeout); + {Error, Label, Reqs1} -> + ErrMsg = "~p : error cleaning indexes db:~p req:~p error:~p", + couch_log:error(ErrMsg, [?MODULE, DbName, Label, Error]), + recv(DbName, Reqs1, Timeout); + no_request -> + ok + end. diff --git a/src/fabric/src/fabric_open_revs.erl b/src/fabric/src/fabric_open_revs.erl index b0a54645a1..b4f95df8a3 100644 --- a/src/fabric/src/fabric_open_revs.erl +++ b/src/fabric/src/fabric_open_revs.erl @@ -83,8 +83,7 @@ handle_message(Reason, Worker, #st{} = St) -> handle_error(Reason, St#st{workers = Workers1, reqs = Reqs1}). init_state(DbName, IdsRevsOpts, Options) -> - DefaultR = integer_to_list(mem3:quorum(DbName)), - R = list_to_integer(couch_util:get_value(r, Options, DefaultR)), + R = fabric_util:r_from_opts(DbName, Options), {ArgRefs, Reqs0} = build_req_map(IdsRevsOpts), ShardMap = build_worker_map(DbName, Reqs0), {Workers, Reqs} = spawn_workers(Reqs0, ShardMap, Options), diff --git a/src/fabric/src/fabric_rpc.erl b/src/fabric/src/fabric_rpc.erl index 67f529e093..31c42c2a94 100644 --- a/src/fabric/src/fabric_rpc.erl +++ b/src/fabric/src/fabric_rpc.erl @@ -33,6 +33,7 @@ reset_validation_funs/1, set_security/3, set_revs_limit/3, + update_props/4, create_shard_db_doc/2, delete_shard_db_doc/2, get_partition_info/2 @@ -146,9 +147,13 @@ all_docs(DbName, Options, Args0) -> case fabric_util:upgrade_mrargs(Args0) of #mrargs{keys = undefined} = Args -> set_io_priority(DbName, Options), - {ok, Db} = get_or_create_db(DbName, Options), - CB = get_view_cb(Args), - couch_mrview:query_all_docs(Db, Args, CB, Args) + case get_or_create_db(DbName, Options) of + {ok, Db} -> + CB = get_view_cb(Args), + couch_mrview:query_all_docs(Db, Args, CB, Args); + Error -> + rexi:reply(Error) + end end. update_mrview(DbName, {DDocId, Rev}, ViewName, Args0) -> @@ -171,9 +176,13 @@ map_view(DbName, {DDocId, Rev}, ViewName, Args0, DbOptions) -> map_view(DbName, DDoc, ViewName, Args0, DbOptions) -> set_io_priority(DbName, DbOptions), Args = fabric_util:upgrade_mrargs(Args0), - {ok, Db} = get_or_create_db(DbName, DbOptions), - CB = get_view_cb(Args), - couch_mrview:query_view(Db, DDoc, ViewName, Args, CB, Args). + case get_or_create_db(DbName, DbOptions) of + {ok, Db} -> + CB = get_view_cb(Args), + couch_mrview:query_view(Db, DDoc, ViewName, Args, CB, Args); + Error -> + rexi:reply(Error) + end. %% @equiv reduce_view(DbName, DDoc, ViewName, Args0) reduce_view(DbName, DDocInfo, ViewName, Args0) -> @@ -185,10 +194,14 @@ reduce_view(DbName, {DDocId, Rev}, ViewName, Args0, DbOptions) -> reduce_view(DbName, DDoc, ViewName, Args0, DbOptions) -> set_io_priority(DbName, DbOptions), Args = fabric_util:upgrade_mrargs(Args0), - {ok, Db} = get_or_create_db(DbName, DbOptions), - VAcc0 = #vacc{db = Db}, - Callback = fun(Msg, Acc) -> reduce_cb(Msg, Acc, Args#mrargs.extra) end, - couch_mrview:query_view(Db, DDoc, ViewName, Args, Callback, VAcc0). + case get_or_create_db(DbName, DbOptions) of + {ok, Db} -> + VAcc0 = #vacc{db = Db}, + Callback = fun(Msg, Acc) -> reduce_cb(Msg, Acc, Args#mrargs.extra) end, + couch_mrview:query_view(Db, DDoc, ViewName, Args, Callback, VAcc0); + Error -> + rexi:reply(Error) + end. create_db(DbName) -> create_db(DbName, []). @@ -262,6 +275,9 @@ set_revs_limit(DbName, Limit, Options) -> set_purge_infos_limit(DbName, Limit, Options) -> with_db(DbName, Options, {couch_db, set_purge_infos_limit, [Limit]}). +update_props(DbName, K, V, Options) -> + with_db(DbName, Options, {couch_db, update_props, [K, V]}). + open_doc(DbName, DocId, Options) -> with_db(DbName, Options, {couch_db, open_doc, [DocId, Options]}). @@ -341,7 +357,7 @@ compact(ShardName, DesignName) -> {ok, Pid} = couch_index_server:get_index( couch_mrview_index, ShardName, <<"_design/", DesignName/binary>> ), - Ref = erlang:make_ref(), + Ref = make_ref(), Pid ! {'$gen_call', {self(), Ref}, compact}. get_uuid(DbName) -> @@ -458,7 +474,7 @@ get_node_seqs(Db, Nodes) -> PurgeSeq = couch_util:get_value(<<"purge_seq">>, Props), case lists:keyfind(TgtNode, 1, Acc) of {_, OldSeq} -> - NewSeq = erlang:max(OldSeq, PurgeSeq), + NewSeq = max(OldSeq, PurgeSeq), NewEntry = {TgtNode, NewSeq}, NewAcc = lists:keyreplace(TgtNode, 1, Acc, NewEntry), {ok, NewAcc}; @@ -470,13 +486,18 @@ get_node_seqs(Db, Nodes) -> {stop, Acc} end end, - InitAcc = [{list_to_binary(atom_to_list(Node)), 0} || Node <- Nodes], + InitAcc = [{atom_to_binary(Node), 0} || Node <- Nodes], Opts = [{start_key, <>}], {ok, NodeBinSeqs} = couch_db:fold_local_docs(Db, FoldFun, InitAcc, Opts), - [{list_to_existing_atom(binary_to_list(N)), S} || {N, S} <- NodeBinSeqs]. + [{binary_to_existing_atom(N), S} || {N, S} <- NodeBinSeqs]. get_or_create_db(DbName, Options) -> - mem3_util:get_or_create_db_int(DbName, Options). + try + mem3_util:get_or_create_db_int(DbName, Options) + catch + throw:{error, missing_target} -> + error(database_does_not_exist, [DbName]) + end. get_view_cb(#mrargs{extra = Options}) -> case couch_util:get_value(callback, Options) of @@ -621,7 +642,7 @@ make_att_reader({follows, Parser, Ref}) when is_pid(Parser) -> % First time encountering a particular parser pid. Monitor it, % in case it dies, and notify it about us, so it could monitor % us in case we die. - PRef = erlang:monitor(process, Parser), + PRef = monitor(process, Parser), put({mp_parser_ref, Parser}, PRef), Parser ! {hello_from_writer, Ref, WriterPid}, PRef; diff --git a/src/fabric/src/fabric_streams.erl b/src/fabric/src/fabric_streams.erl index 3f9cbdc494..df96ae67cd 100644 --- a/src/fabric/src/fabric_streams.erl +++ b/src/fabric/src/fabric_streams.erl @@ -198,7 +198,7 @@ spawn_worker_cleaner(Coordinator, Workers, ClientReq) when case get(?WORKER_CLEANER) of undefined -> Pid = spawn(fun() -> - erlang:monitor(process, Coordinator), + monitor(process, Coordinator), NodeRefSet = couch_util:set_from_list(shards_to_node_refs(Workers)), cleaner_loop(Coordinator, NodeRefSet, ClientReq) end), @@ -269,7 +269,7 @@ should_clean_workers(_) -> end end), Cleaner = spawn_worker_cleaner(Coord, Workers, undefined), - Ref = erlang:monitor(process, Cleaner), + Ref = monitor(process, Cleaner), Coord ! die, receive {'DOWN', Ref, _, Cleaner, _} -> ok @@ -289,7 +289,7 @@ does_not_fire_if_cleanup_called(_) -> end end), Cleaner = spawn_worker_cleaner(Coord, Workers, undefined), - Ref = erlang:monitor(process, Cleaner), + Ref = monitor(process, Cleaner), cleanup(Workers), Coord ! die, receive @@ -312,7 +312,7 @@ should_clean_additional_worker_too(_) -> end), Cleaner = spawn_worker_cleaner(Coord, Workers, undefined), add_worker_to_cleaner(Coord, #shard{node = 'n2', ref = make_ref()}), - Ref = erlang:monitor(process, Cleaner), + Ref = monitor(process, Cleaner), Coord ! die, receive {'DOWN', Ref, _, Cleaner, _} -> ok @@ -337,7 +337,7 @@ coordinator_is_killed_if_client_disconnects(_) -> % Close the socket and then expect coordinator to be killed ok = gen_tcp:close(Sock), Cleaner = spawn_worker_cleaner(Coord, Workers, ClientReq), - CleanerRef = erlang:monitor(process, Cleaner), + CleanerRef = monitor(process, Cleaner), % Assert the correct behavior on the support platforms (all except Windows so far) case os:type() of {unix, Type} when @@ -378,7 +378,7 @@ coordinator_is_not_killed_if_client_is_connected(_) -> {ok, Sock} = gen_tcp:listen(0, [{active, false}]), ClientReq = mochiweb_request:new(Sock, 'GET', "/foo", {1, 1}, Headers), Cleaner = spawn_worker_cleaner(Coord, Workers, ClientReq), - CleanerRef = erlang:monitor(process, Cleaner), + CleanerRef = monitor(process, Cleaner), % Coordinator should stay up receive {'DOWN', CoordRef, _, Coord, _} -> @@ -430,7 +430,7 @@ submit_jobs_sets_up_cleaner(_) -> meck:wait(2, rexi, cast_ref, '_', 1000), % If we kill the coordinator, the cleaner should kill the workers meck:reset(rexi), - CleanupMon = erlang:monitor(process, Cleaner), + CleanupMon = monitor(process, Cleaner), exit(Coord, kill), receive {'DOWN', CoordRef, _, _, WorkerReason} -> diff --git a/src/fabric/src/fabric_util.erl b/src/fabric/src/fabric_util.erl index d0961533f3..1da214f7fe 100644 --- a/src/fabric/src/fabric_util.erl +++ b/src/fabric/src/fabric_util.erl @@ -26,6 +26,7 @@ doc_id_and_rev/1 ]). -export([request_timeout/0, attachments_timeout/0, all_docs_timeout/0, view_timeout/1, timeout/2]). +-export([abs_request_timeout/0]). -export([log_timeout/2, remove_done_workers/2]). -export([is_users_db/1, is_replicator_db/1]). -export([open_cluster_db/1, open_cluster_db/2]). @@ -35,13 +36,14 @@ -export([worker_ranges/1]). -export([get_uuid_prefix_len/0]). -export([isolate/1, isolate/2]). +-export([get_design_doc_records/1]). +-export([w_from_opts/2, r_from_opts/2]). -compile({inline, [{doc_id_and_rev, 1}]}). -include_lib("mem3/include/mem3.hrl"). -include_lib("couch/include/couch_db.hrl"). -include_lib("couch_mrview/include/couch_mrview.hrl"). --include_lib("eunit/include/eunit.hrl"). remove_down_workers(Workers, BadNode) -> remove_down_workers(Workers, BadNode, []). @@ -107,6 +109,15 @@ log_timeout(Workers, EndPoint) -> Workers ). +% Return {abs, MonotonicMSec}. This is a format used by erpc to +% provide an absolute time limit for a collection or requests +% See https://www.erlang.org/doc/apps/kernel/erpc.html#t:timeout_time/0 +% +abs_request_timeout() -> + Timeout = fabric_util:request_timeout(), + NowMSec = erlang:monotonic_time(millisecond), + {abs, NowMSec + Timeout}. + remove_done_workers(Workers, WaitingIndicator) -> [W || {W, WI} <- fabric_dict:to_list(Workers), WI == WaitingIndicator]. @@ -120,7 +131,7 @@ get_db(DbName, Options) -> Shards = Local ++ lists:keysort(#shard.name, SameZone) ++ lists:keysort(#shard.name, DifferentZone), % suppress shards from down nodes - Nodes = [node() | erlang:nodes()], + Nodes = [node() | nodes()], Live = [S || #shard{node = N} = S <- Shards, lists:member(N, Nodes)], % Only accept factors > 1, otherwise our math breaks further down Factor = max(2, config:get_integer("fabric", "shard_timeout_factor", 2)), @@ -130,7 +141,7 @@ get_db(DbName, Options) -> get_shard(Live, Options, Timeout, Factor). get_shard([], _Opts, _Timeout, _Factor) -> - erlang:error({internal_server_error, "No DB shards could be opened."}); + error({internal_server_error, "No DB shards could be opened."}); get_shard([#shard{node = Node, name = Name} | Rest], Opts, Timeout, Factor) -> Mon = rexi_monitor:start([rexi_utils:server_pid(Node)]), MFA = {fabric_rpc, open_shard, [Name, [{timeout, Timeout} | Opts]]}, @@ -226,7 +237,7 @@ remove_ancestors([{_, {{not_found, _}, Count}} = Head | Tail], Acc) -> remove_ancestors([{_, {{ok, #doc{revs = {Pos, Revs}}}, Count}} = Head | Tail], Acc) -> Descendants = lists:dropwhile( fun({_, {{ok, #doc{revs = {Pos2, Revs2}}}, _}}) -> - case lists:nthtail(erlang:min(Pos2 - Pos, length(Revs2)), Revs2) of + case lists:nthtail(min(Pos2 - Pos, length(Revs2)), Revs2) of [] -> % impossible to tell if Revs2 is a descendant - assume no true; @@ -250,38 +261,6 @@ create_monitors(Shards) -> MonRefs = lists:usort([rexi_utils:server_pid(N) || #shard{node = N} <- Shards]), rexi_monitor:start(MonRefs). -%% verify only id and rev are used in key. -update_counter_test() -> - Reply = - {ok, #doc{ - id = <<"id">>, - revs = <<"rev">>, - body = <<"body">>, - atts = <<"atts">> - }}, - ?assertEqual( - [{{<<"id">>, <<"rev">>}, {Reply, 1}}], - update_counter(Reply, 1, []) - ). - -remove_ancestors_test() -> - Foo1 = {ok, #doc{revs = {1, [<<"foo">>]}}}, - Foo2 = {ok, #doc{revs = {2, [<<"foo2">>, <<"foo">>]}}}, - Bar1 = {ok, #doc{revs = {1, [<<"bar">>]}}}, - Bar2 = {not_found, {1, <<"bar">>}}, - ?assertEqual( - [kv(Bar1, 1), kv(Foo1, 1)], - remove_ancestors([kv(Bar1, 1), kv(Foo1, 1)], []) - ), - ?assertEqual( - [kv(Bar1, 1), kv(Foo2, 2)], - remove_ancestors([kv(Bar1, 1), kv(Foo1, 1), kv(Foo2, 1)], []) - ), - ?assertEqual( - [kv(Bar1, 2)], - remove_ancestors([kv(Bar2, 1), kv(Bar1, 1)], []) - ). - is_replicator_db(DbName) -> path_ends_with(DbName, <<"_replicator">>). @@ -296,15 +275,12 @@ is_users_db(DbName) -> path_ends_with(Path, Suffix) -> Suffix =:= couch_db:dbname_suffix(Path). -open_cluster_db(#shard{dbname = DbName, opts = Options}) -> - case couch_util:get_value(props, Options) of - Props when is_list(Props) -> - {ok, Db} = couch_db:clustered_db(DbName, [{props, Props}]), - Db; - _ -> - {ok, Db} = couch_db:clustered_db(DbName, []), - Db - end. +open_cluster_db(#shard{dbname = DbName}) -> + open_cluster_db(DbName); +open_cluster_db(DbName) when is_binary(DbName) -> + Props = mem3:props(DbName), + {ok, Db} = couch_db:clustered_db(DbName, [{props, Props}]), + Db. open_cluster_db(DbName, Opts) -> % as admin @@ -320,25 +296,22 @@ kv(Item, Count) -> doc_id_and_rev(#doc{id = DocId, revs = {RevNum, [RevHash | _]}}) -> {DocId, {RevNum, RevHash}}. -is_partitioned(DbName0) when is_binary(DbName0) -> - Shards = mem3:shards(fabric:dbname(DbName0)), - is_partitioned(open_cluster_db(hd(Shards))); +is_partitioned(DbName) when is_binary(DbName) -> + is_partitioned(open_cluster_db(DbName)); is_partitioned(Db) -> couch_db:is_partitioned(Db). validate_all_docs_args(DbName, Args) when is_list(DbName) -> validate_all_docs_args(list_to_binary(DbName), Args); validate_all_docs_args(DbName, Args) when is_binary(DbName) -> - Shards = mem3:shards(fabric:dbname(DbName)), - Db = open_cluster_db(hd(Shards)), + Db = open_cluster_db(DbName), validate_all_docs_args(Db, Args); validate_all_docs_args(Db, Args) -> true = couch_db:is_clustered(Db), couch_mrview_util:validate_all_docs_args(Db, Args). validate_args(DbName, DDoc, Args) when is_binary(DbName) -> - Shards = mem3:shards(fabric:dbname(DbName)), - Db = open_cluster_db(hd(Shards)), + Db = open_cluster_db(DbName), validate_args(Db, DDoc, Args); validate_args(Db, DDoc, Args) -> true = couch_db:is_clustered(Db), @@ -397,6 +370,43 @@ worker_ranges(Workers) -> get_uuid_prefix_len() -> config:get_integer("fabric", "uuid_prefix_len", 7). +% Get design #doc{} records. Run in an isolated process. This is often used +% when computing signatures of various indexes +% +get_design_doc_records(DbName) -> + fabric_util:isolate(fun() -> + case fabric:design_docs(DbName) of + {ok, DDocs} when is_list(DDocs) -> + Fun = fun({[_ | _]} = Doc) -> couch_doc:from_json_obj(Doc) end, + {ok, lists:map(Fun, DDocs)}; + Else -> + Else + end + end). + +w_from_opts(Db, Options) -> + quorum_from_opts(Db, couch_util:get_value(w, Options)). + +r_from_opts(Db, Options) -> + quorum_from_opts(Db, couch_util:get_value(r, Options)). + +quorum_from_opts(Db, Val) -> + try + if + is_integer(Val) -> + Val; + is_list(Val) -> + % Compatibility clause. Keep as long as chttpd parses r and w + % request parameters as lists (strings). + list_to_integer(Val); + true -> + mem3:quorum(Db) + end + catch + _:_ -> + mem3:quorum(Db) + end. + % If we issue multiple fabric calls from the same process we have to isolate % them so in case of error they don't pollute the processes dictionary or the % mailbox @@ -405,16 +415,16 @@ isolate(Fun) -> isolate(Fun, infinity). isolate(Fun, Timeout) -> - {Pid, Ref} = erlang:spawn_monitor(fun() -> exit(do_isolate(Fun)) end), + {Pid, Ref} = spawn_monitor(fun() -> exit(do_isolate(Fun)) end), receive {'DOWN', Ref, _, _, {'$isolres', Res}} -> Res; {'DOWN', Ref, _, _, {'$isolerr', Tag, Reason, Stack}} -> erlang:raise(Tag, Reason, Stack) after Timeout -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), exit(Pid, kill), - erlang:error(timeout) + error(timeout) end. do_isolate(Fun) -> @@ -425,6 +435,41 @@ do_isolate(Fun) -> {'$isolerr', Tag, Reason, Stack} end. +-ifdef(TEST). +-include_lib("couch/include/couch_eunit.hrl"). + +%% verify only id and rev are used in key. +update_counter_test() -> + Reply = + {ok, #doc{ + id = <<"id">>, + revs = <<"rev">>, + body = <<"body">>, + atts = <<"atts">> + }}, + ?assertEqual( + [{{<<"id">>, <<"rev">>}, {Reply, 1}}], + update_counter(Reply, 1, []) + ). + +remove_ancestors_test() -> + Foo1 = {ok, #doc{revs = {1, [<<"foo">>]}}}, + Foo2 = {ok, #doc{revs = {2, [<<"foo2">>, <<"foo">>]}}}, + Bar1 = {ok, #doc{revs = {1, [<<"bar">>]}}}, + Bar2 = {not_found, {1, <<"bar">>}}, + ?assertEqual( + [kv(Bar1, 1), kv(Foo1, 1)], + remove_ancestors([kv(Bar1, 1), kv(Foo1, 1)], []) + ), + ?assertEqual( + [kv(Bar1, 1), kv(Foo2, 2)], + remove_ancestors([kv(Bar1, 1), kv(Foo1, 1), kv(Foo2, 1)], []) + ), + ?assertEqual( + [kv(Bar1, 2)], + remove_ancestors([kv(Bar2, 1), kv(Bar1, 1)], []) + ). + get_db_timeout_test() -> % Q=1, N=1 ?assertEqual(20000, get_db_timeout(1, 2, 100, 60000)), @@ -468,3 +513,32 @@ get_db_timeout_test() -> % request_timeout was set to infinity, with enough shards it still gets to % 100 min timeout at the start from the exponential logic ?assertEqual(100, get_db_timeout(64, 2, 100, infinity)). + +rw_opts_test_() -> + { + foreach, + fun() -> meck:new(mem3, [passthrough]) end, + fun(_) -> meck:unload() end, + [ + ?TDEF_FE(t_w_opts_get), + ?TDEF_FE(t_r_opts_get) + ] + }. + +t_w_opts_get(_) -> + meck:expect(mem3, quorum, 1, 3), + ?assertEqual(5, w_from_opts(any_db, [{w, 5}])), + ?assertEqual(5, w_from_opts(any_db, [{w, "5"}])), + ?assertEqual(3, w_from_opts(any_db, [{w, some_other_type}])), + ?assertEqual(3, w_from_opts(any_db, [{w, "five"}])), + ?assertEqual(3, w_from_opts(any_db, [])). + +t_r_opts_get(_) -> + meck:expect(mem3, quorum, 1, 3), + ?assertEqual(5, r_from_opts(any_db, [{other_opt, 42}, {r, 5}])), + ?assertEqual(5, r_from_opts(any_db, [{r, "5"}, {something_else, "xyz"}])), + ?assertEqual(3, r_from_opts(any_db, [{r, some_other_type}])), + ?assertEqual(3, r_from_opts(any_db, [{r, "five"}])), + ?assertEqual(3, r_from_opts(any_db, [])). + +-endif. diff --git a/src/fabric/src/fabric_view_all_docs.erl b/src/fabric/src/fabric_view_all_docs.erl index 2d0133acb5..973e82e98d 100644 --- a/src/fabric/src/fabric_view_all_docs.erl +++ b/src/fabric/src/fabric_view_all_docs.erl @@ -144,7 +144,7 @@ go(DbName, _Options, Workers, QueryArgs, Callback, Acc0) -> fun handle_message/3, State, fabric_util:view_timeout(QueryArgs), - 5000 + fabric_util:timeout("all_docs_view_permsg", "5000") ) of {ok, NewState} -> @@ -239,7 +239,7 @@ handle_message({meta, Meta0}, {Worker, From}, State) -> FinalOffset = case Offset of null -> null; - _ -> erlang:min(Total, Offset + State#collector.skip) + _ -> min(Total, Offset + State#collector.skip) end, Meta = [{total, Total}, {offset, FinalOffset}] ++ @@ -373,7 +373,7 @@ cancel_read_pids(Pids) -> case queue:out(Pids) of {{value, {Pid, Ref}}, RestPids} -> exit(Pid, kill), - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), cancel_read_pids(RestPids); {empty, _} -> ok diff --git a/src/fabric/src/fabric_view_changes.erl b/src/fabric/src/fabric_view_changes.erl index f6695f1630..40e0522688 100644 --- a/src/fabric/src/fabric_view_changes.erl +++ b/src/fabric/src/fabric_view_changes.erl @@ -388,7 +388,7 @@ pack_seqs(Workers) -> SeqList = [{N, R, S} || {#shard{node = N, range = R}, S} <- Workers], SeqSum = lists:sum([fake_packed_seq(S) || {_, _, S} <- SeqList]), Opaque = couch_util:encodeBase64Url(?term_to_bin(SeqList, [compressed])), - ?l2b([integer_to_list(SeqSum), $-, Opaque]). + <<(integer_to_binary(SeqSum))/binary, $-, Opaque/binary>>. % Generate the sequence number used to build the emitted N-... prefix. % @@ -536,7 +536,7 @@ get_old_seq(#shard{range = R} = Shard, SinceSeqs) -> get_db_uuid_shards(DbName) -> % Need to use an isolated process as we are performing a fabric call from - % another fabric call and there is a good chance we'd polute the mailbox + % another fabric call and there is a good chance we'd pollute the mailbox % with returned messages Timeout = fabric_util:request_timeout(), IsolatedFun = fun() -> fabric:db_uuids(DbName) end, diff --git a/src/fabric/src/fabric_view_map.erl b/src/fabric/src/fabric_view_map.erl index cc8ed6cf1c..56389e7df1 100644 --- a/src/fabric/src/fabric_view_map.erl +++ b/src/fabric/src/fabric_view_map.erl @@ -181,7 +181,7 @@ handle_message({meta, Meta0}, {Worker, From}, State) -> offset = Offset }}; false -> - FinalOffset = erlang:min(Total, Offset + State#collector.skip), + FinalOffset = min(Total, Offset + State#collector.skip), Meta = [{total, Total}, {offset, FinalOffset}] ++ case UpdateSeq of diff --git a/src/fabric/test/eunit/fabric_bench_test.erl b/src/fabric/test/eunit/fabric_bench_test.erl index ea514cce8c..f055d24da0 100644 --- a/src/fabric/test/eunit/fabric_bench_test.erl +++ b/src/fabric/test/eunit/fabric_bench_test.erl @@ -59,7 +59,7 @@ t_old_db_deletion_works(_Ctx) -> % Quick db creation and deletion is racy so % we have to wait until the db is gone before proceeding. WaitFun = fun() -> - try mem3_shards:opts_for_db(Db) of + try mem3:props(Db) of _ -> wait catch error:database_does_not_exist -> diff --git a/src/fabric/test/eunit/fabric_db_info_tests.erl b/src/fabric/test/eunit/fabric_db_info_tests.erl index e7df560a1a..9a133ace58 100644 --- a/src/fabric/test/eunit/fabric_db_info_tests.erl +++ b/src/fabric/test/eunit/fabric_db_info_tests.erl @@ -20,7 +20,8 @@ main_test_() -> fun setup/0, fun teardown/1, with([ - ?TDEF(t_update_seq_has_uuids) + ?TDEF(t_update_seq_has_uuids), + ?TDEF(t_update_and_get_props) ]) }. @@ -55,3 +56,38 @@ t_update_seq_has_uuids(_) -> ?assertEqual(UuidFromShard, SeqUuid), ok = fabric:delete_db(DbName, []). + +t_update_and_get_props(_) -> + DbName = ?tempdb(), + ok = fabric:create_db(DbName, [{q, 1}, {n, 1}]), + + {ok, Info} = fabric:get_db_info(DbName), + Props = couch_util:get_value(props, Info), + ?assertEqual({[]}, Props), + + ?assertEqual(ok, fabric:update_props(DbName, <<"foo">>, 100)), + {ok, Info1} = fabric:get_db_info(DbName), + Props1 = couch_util:get_value(props, Info1), + ?assertEqual({[{<<"foo">>, 100}]}, Props1), + + ?assertEqual(ok, fabric:update_props(DbName, bar, 101)), + {ok, Info2} = fabric:get_db_info(DbName), + Props2 = couch_util:get_value(props, Info2), + ?assertEqual( + {[ + {<<"foo">>, 100}, + {bar, 101} + ]}, + Props2 + ), + + ?assertEqual(ok, fabric:update_props(DbName, <<"foo">>, undefined)), + {ok, Info3} = fabric:get_db_info(DbName), + ?assertEqual({[{bar, 101}]}, couch_util:get_value(props, Info3)), + + Res = fabric:update_props(DbName, partitioned, true), + ?assertMatch({error, {bad_request, _}}, Res), + {ok, Info4} = fabric:get_db_info(DbName), + ?assertEqual({[{bar, 101}]}, couch_util:get_value(props, Info4)), + + ok = fabric:delete_db(DbName, []). diff --git a/src/fabric/test/eunit/fabric_moved_shards_seq_tests.erl b/src/fabric/test/eunit/fabric_moved_shards_seq_tests.erl index c3bb0c880e..f46ff927be 100644 --- a/src/fabric/test/eunit/fabric_moved_shards_seq_tests.erl +++ b/src/fabric/test/eunit/fabric_moved_shards_seq_tests.erl @@ -39,7 +39,7 @@ t_shard_moves_avoid_sequence_rewinds(_) -> ok = fabric:create_db(DbName, [{q, 1}, {n, 1}]), lists:foreach( fun(I) -> - update_doc(DbName, #doc{id = erlang:integer_to_binary(I)}) + update_doc(DbName, #doc{id = integer_to_binary(I)}) end, lists:seq(1, DocCnt) ), diff --git a/src/fabric/test/eunit/fabric_rpc_purge_tests.erl b/src/fabric/test/eunit/fabric_rpc_purge_tests.erl index 07e6b1d422..fa6123dfcc 100644 --- a/src/fabric/test/eunit/fabric_rpc_purge_tests.erl +++ b/src/fabric/test/eunit/fabric_rpc_purge_tests.erl @@ -152,7 +152,7 @@ t_no_filter_different_node({DbName, DocId, OldDoc, PSeq}) -> create_purge_checkpoint(DbName, PSeq), % Create a valid purge for a different node - TgtNode = list_to_binary(atom_to_list('notfoo@127.0.0.1')), + TgtNode = atom_to_binary('notfoo@127.0.0.1'), create_purge_checkpoint(DbName, 0, TgtNode), rpc_update_doc(DbName, OldDoc), @@ -164,7 +164,7 @@ t_filter_local_node({DbName, DocId, OldDoc, PSeq}) -> create_purge_checkpoint(DbName, PSeq), % Create a valid purge for a different node - TgtNode = list_to_binary(atom_to_list('notfoo@127.0.0.1')), + TgtNode = atom_to_binary('notfoo@127.0.0.1'), create_purge_checkpoint(DbName, 0, TgtNode), % Add a local node rev to the list of node revs. It should @@ -257,7 +257,7 @@ rpc_update_doc(DbName, Doc) -> rpc_update_doc(DbName, Doc, [RROpt]). rpc_update_doc(DbName, Doc, Opts) -> - Ref = erlang:make_ref(), + Ref = make_ref(), put(rexi_from, {self(), Ref}), fabric_rpc:update_docs(DbName, [Doc], Opts), Reply = test_util:wait(fun() -> @@ -274,4 +274,4 @@ tgt_node() -> 'foo@127.0.0.1'. tgt_node_bin() -> - iolist_to_binary(atom_to_list(tgt_node())). + atom_to_binary(tgt_node()). diff --git a/src/fabric/test/eunit/fabric_rpc_tests.erl b/src/fabric/test/eunit/fabric_rpc_tests.erl index 16bb66bada..39c86e4c47 100644 --- a/src/fabric/test/eunit/fabric_rpc_tests.erl +++ b/src/fabric/test/eunit/fabric_rpc_tests.erl @@ -101,7 +101,7 @@ t_no_config_db_create_fails_for_shard_rpc(DbName) -> receive Resp0 -> Resp0 end, - ?assertMatch({Ref, {'rexi_EXIT', {{error, missing_target}, _}}}, Resp). + ?assertMatch({Ref, {'rexi_EXIT', {database_does_not_exist, _}}}, Resp). t_db_create_with_config(DbName) -> MDbName = mem3:dbname(DbName), diff --git a/src/fabric/test/eunit/fabric_tests.erl b/src/fabric/test/eunit/fabric_tests.erl index 1ba5d1bc6a..77327f4458 100644 --- a/src/fabric/test/eunit/fabric_tests.erl +++ b/src/fabric/test/eunit/fabric_tests.erl @@ -47,17 +47,20 @@ teardown({Ctx, DbName}) -> test_util:stop_couch(Ctx). t_cleanup_index_files(_) -> - CheckFun = fun(Res) -> Res =:= ok end, - ?assert(lists:all(CheckFun, fabric:cleanup_index_files())). + ?assertEqual(ok, fabric:cleanup_index_files_this_node()), + ?assertEqual(ok, fabric:cleanup_index_files_all_nodes()). t_cleanup_index_files_with_existing_db({_, DbName}) -> - ?assertEqual(ok, fabric:cleanup_index_files(DbName)). + ?assertEqual(ok, fabric:cleanup_index_files_this_node(DbName)), + ?assertEqual(ok, fabric:cleanup_index_files_all_nodes(DbName)), + ?assertEqual(ok, fabric:cleanup_index_files_this_node(<<"non_existent">>)), + ?assertEqual(ok, fabric:cleanup_index_files_all_nodes(<<"non_existent">>)). t_cleanup_index_files_with_view_data({_, DbName}) -> Sigs = sigs(DbName), Indices = indices(DbName), Purges = purges(DbName), - ok = fabric:cleanup_index_files(DbName), + ok = fabric:cleanup_index_files_all_nodes(DbName), % We haven't inadvertently removed any active index bits ?assertEqual(Sigs, sigs(DbName)), ?assertEqual(Indices, indices(DbName)), @@ -65,7 +68,7 @@ t_cleanup_index_files_with_view_data({_, DbName}) -> t_cleanup_index_files_with_deleted_db(_) -> SomeDb = ?tempdb(), - ?assertEqual(ok, fabric:cleanup_index_files(SomeDb)). + ?assertEqual(ok, fabric:cleanup_index_files_all_nodes(SomeDb)). t_cleanup_index_file_after_ddoc_update({_, DbName}) -> ?assertEqual( @@ -84,7 +87,7 @@ t_cleanup_index_file_after_ddoc_update({_, DbName}) -> ), update_ddoc(DbName, <<"_design/foo">>, <<"bar1">>), - ok = fabric:cleanup_index_files(DbName), + ok = fabric:cleanup_index_files_all_nodes(DbName), {ok, _} = fabric:query_view(DbName, <<"foo">>, <<"bar1">>), % One 4bc stays, da8 should gone and 9e3 is added @@ -120,7 +123,7 @@ t_cleanup_index_file_after_ddoc_delete({_, DbName}) -> ), delete_ddoc(DbName, <<"_design/foo">>), - ok = fabric:cleanup_index_files(DbName), + ok = fabric:cleanup_index_files_all_nodes(DbName), % 4bc stays the same, da8 should be gone ?assertEqual( @@ -137,13 +140,13 @@ t_cleanup_index_file_after_ddoc_delete({_, DbName}) -> ), delete_ddoc(DbName, <<"_design/boo">>), - ok = fabric:cleanup_index_files(DbName), + ok = fabric:cleanup_index_files_all_nodes(DbName), ?assertEqual([], indices(DbName)), ?assertEqual([], purges(DbName)), % cleaning a db with all deleted indices should still work - ok = fabric:cleanup_index_files(DbName), + ok = fabric:cleanup_index_files_all_nodes(DbName), ?assertEqual([], indices(DbName)), ?assertEqual([], purges(DbName)). @@ -298,7 +301,7 @@ t_query_view_configuration({_Ctx, DbName}) -> view_type = map, start_key_docid = <<>>, end_key_docid = <<255>>, - extra = [{view_row_map, true}] + extra = [{validated, true}, {view_row_map, true}] }, Options = [], Accumulator = [], diff --git a/src/global_changes/src/global_changes_server.erl b/src/global_changes/src/global_changes_server.erl index 8be0552f1b..8f502df8ee 100644 --- a/src/global_changes/src/global_changes_server.erl +++ b/src/global_changes/src/global_changes_server.erl @@ -69,7 +69,7 @@ init([]) -> pending_updates = couch_util:new_set(), max_write_delay = MaxWriteDelay, dbname = GlobalChangesDbName, - handler_ref = erlang:monitor(process, Handler) + handler_ref = monitor(process, Handler) }, {ok, State}. @@ -122,7 +122,7 @@ handle_info(flush_updates, State) -> handle_info(start_listener, State) -> {ok, Handler} = global_changes_listener:start(), NewState = State#state{ - handler_ref = erlang:monitor(process, Handler) + handler_ref = monitor(process, Handler) }, {noreply, NewState}; handle_info({'DOWN', Ref, _, _, Reason}, #state{handler_ref = Ref} = State) -> diff --git a/src/ioq/src/ioq.erl b/src/ioq/src/ioq.erl index 8e38c2a001..031626cfed 100644 --- a/src/ioq/src/ioq.erl +++ b/src/ioq/src/ioq.erl @@ -146,7 +146,7 @@ handle_cast(_Msg, State) -> handle_info({Ref, Reply}, State) -> case lists:keytake(Ref, #request.ref, State#state.running) of {value, Request, Remaining} -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), gen_server:reply(Request#request.from, Reply), {noreply, State#state{running = Remaining}, 0}; false -> @@ -225,6 +225,6 @@ choose_next_request(Index, State) -> end. submit_request(#request{} = Request, #state{} = State) -> - Ref = erlang:monitor(process, Request#request.fd), + Ref = monitor(process, Request#request.fd), Request#request.fd ! {'$gen_call', {self(), Ref}, Request#request.msg}, State#state{running = [Request#request{ref = Ref} | State#state.running]}. diff --git a/src/ken/rebar.config.script b/src/ken/rebar.config.script index d219b606ac..61af64c6d9 100644 --- a/src/ken/rebar.config.script +++ b/src/ken/rebar.config.script @@ -12,19 +12,9 @@ % License for the specific language governing permissions and limitations under % the License. -HaveDreyfus = element(1, file:list_dir("../dreyfus")) == ok. - -HastingsHome = os:getenv("HASTINGS_HOME", "../hastings"). -HaveHastings = element(1, file:list_dir(HastingsHome)) == ok. - -CurrOpts = case lists:keyfind(erl_opts, 1, CONFIG) of +ErlOpts = [{i, "../"}] ++ case lists:keyfind(erl_opts, 1, CONFIG) of {erl_opts, Opts} -> Opts; false -> [] end, -NewOpts = - if HaveDreyfus -> [{d, 'HAVE_DREYFUS'}]; true -> [] end ++ - if HaveHastings -> [{d, 'HAVE_HASTINGS'}]; true -> [] end ++ - [{i, "../"}] ++ CurrOpts. - -lists:keystore(erl_opts, 1, CONFIG, {erl_opts, NewOpts}). +lists:keystore(erl_opts, 1, CONFIG, {erl_opts, ErlOpts}). diff --git a/src/ken/src/ken.app.src.script b/src/ken/src/ken.app.src.script index aad00a7b9c..f88d05944e 100644 --- a/src/ken/src/ken.app.src.script +++ b/src/ken/src/ken.app.src.script @@ -10,23 +10,16 @@ % License for the specific language governing permissions and limitations under % the License. -HaveDreyfus = code:lib_dir(dreyfus) /= {error, bad_name}. -HaveHastings = code:lib_dir(hastings) /= {error, bad_name}. - -BaseApplications = [ +Applications = [ kernel, stdlib, couch_log, couch_event, couch, - config + config, + dreyfus ]. -Applications = - if HaveDreyfus -> [dreyfus]; true -> [] end ++ - if HaveHastings -> [hastings]; true -> [] end ++ - BaseApplications. - {application, ken, [ {description, "Ken builds views and search indexes automatically"}, {vsn, git}, diff --git a/src/ken/src/ken_server.erl b/src/ken/src/ken_server.erl index 72c0db8efe..1bc7faea19 100644 --- a/src/ken/src/ken_server.erl +++ b/src/ken/src/ken_server.erl @@ -51,13 +51,7 @@ -include_lib("couch/include/couch_db.hrl"). -include_lib("mem3/include/mem3.hrl"). --ifdef(HAVE_DREYFUS). -include_lib("dreyfus/include/dreyfus.hrl"). --endif. - --ifdef(HAVE_HASTINGS). --include_lib("hastings/src/hastings.hrl"). --endif. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -145,20 +139,9 @@ handle_cast({remove, DbName}, State) -> handle_cast({resubmit, DbName}, State) -> ets:delete(ken_resubmit, DbName), handle_cast({add, DbName}, State); -% st index job names have 3 elements, 3rd being 'hastings'. See job record definition. -handle_cast({trigger_update, #job{name = {_, _, hastings}, server = GPid, seq = Seq} = Job}, State) -> - % hastings_index:await will trigger a hastings index update - {Pid, _} = erlang:spawn_monitor( - hastings_index, - await, - [GPid, Seq] - ), - Now = erlang:monotonic_time(), - ets:insert(ken_workers, Job#job{worker_pid = Pid, lru = Now}), - {noreply, State, 0}; handle_cast({trigger_update, #job{name = {_, Index, nouveau}} = Job}, State) -> % nouveau_index_manager:update_index will trigger a search index update. - {Pid, _} = erlang:spawn_monitor( + {Pid, _} = spawn_monitor( nouveau_index_manager, update_index, [Index] @@ -169,7 +152,7 @@ handle_cast({trigger_update, #job{name = {_, Index, nouveau}} = Job}, State) -> % search index job names have 3 elements. See job record definition. handle_cast({trigger_update, #job{name = {_, _, _}, server = GPid, seq = Seq} = Job}, State) -> % dreyfus_index:await will trigger a search index update. - {Pid, _} = erlang:spawn_monitor( + {Pid, _} = spawn_monitor( dreyfus_index, await, [GPid, Seq] @@ -179,7 +162,7 @@ handle_cast({trigger_update, #job{name = {_, _, _}, server = GPid, seq = Seq} = {noreply, State, 0}; handle_cast({trigger_update, #job{name = {_, _}, server = SrvPid, seq = Seq} = Job}, State) -> % couch_index:get_state/2 will trigger a view group index update. - {Pid, _} = erlang:spawn_monitor(couch_index, get_state, [SrvPid, Seq]), + {Pid, _} = spawn_monitor(couch_index, get_state, [SrvPid, Seq]), Now = erlang:monotonic_time(), ets:insert(ken_workers, Job#job{worker_pid = Pid, lru = Now}), {noreply, State, 0}; @@ -294,6 +277,12 @@ design_docs(Name) -> case fabric:design_docs(mem3:dbname(Name)) of {error, {maintenance_mode, _, _Node}} -> {ok, []}; + {error, {nodedown, _Reason}} -> + {ok, []}; + {ok, DDocs} when is_list(DDocs) -> + {ok, DDocs}; + {ok, _Resp} -> + {ok, []}; Else -> Else end @@ -324,18 +313,16 @@ update_ddoc_indexes(Name, #doc{} = Doc, State) -> ok end, SearchUpdated = search_updated(Name, Doc, Seq, State), - STUpdated = st_updated(Name, Doc, Seq, State), NouveauUpdated = nouveau_updated(Name, Doc, Seq, State), - case {ViewUpdated, SearchUpdated, STUpdated, NouveauUpdated} of - {ok, ok, ok, ok} -> ok; + case {ViewUpdated, SearchUpdated, NouveauUpdated} of + {ok, ok, ok} -> ok; _ -> resubmit end. --ifdef(HAVE_DREYFUS). search_updated(Name, Doc, Seq, State) -> case should_update(Doc, <<"indexes">>) of true -> - try dreyfus_index:design_doc_to_indexes(Doc) of + try dreyfus_index:design_doc_to_indexes(Name, Doc) of SIndexes -> update_ddoc_search_indexes(Name, SIndexes, Seq, State) catch _:_ -> @@ -344,28 +331,6 @@ search_updated(Name, Doc, Seq, State) -> false -> ok end. --else. -search_updated(_Name, _Doc, _Seq, _State) -> - ok. --endif. - --ifdef(HAVE_HASTINGS). -st_updated(Name, Doc, Seq, State) -> - case should_update(Doc, <<"st_indexes">>) of - true -> - try hastings_index:design_doc_to_indexes(Doc) of - STIndexes -> update_ddoc_st_indexes(Name, STIndexes, Seq, State) - catch - _:_ -> - ok - end; - false -> - ok - end. --else. -st_updated(_Name, _Doc, _Seq, _State) -> - ok. --endif. nouveau_updated(Name, Doc, Seq, State) -> case should_update(Doc, <<"nouveau">>) of @@ -408,7 +373,6 @@ update_ddoc_views(Name, MRSt, Seq, State) -> ok end. --ifdef(HAVE_DREYFUS). update_ddoc_search_indexes(DbName, Indexes, Seq, State) -> if Indexes =/= [] -> @@ -432,34 +396,6 @@ update_ddoc_search_indexes(DbName, Indexes, Seq, State) -> true -> ok end. --endif. - --ifdef(HAVE_HASTINGS). -update_ddoc_st_indexes(DbName, Indexes, Seq, State) -> - if - Indexes =/= [] -> - % The record name in hastings is #h_idx rather than #index as it is for dreyfus - % Spawn a job for each spatial index in the ddoc - lists:foldl( - fun(#h_idx{ddoc_id = DDocName} = Index, Acc) -> - case hastings_index_manager:get_index(DbName, Index) of - {ok, Pid} -> - case maybe_start_job({DbName, DDocName, hastings}, Pid, Seq, State) of - resubmit -> resubmit; - _ -> Acc - end; - _ -> - % If any job fails, retry the db. - resubmit - end - end, - ok, - Indexes - ); - true -> - ok - end. --endif. update_ddoc_nouveau_indexes(DbName, Indexes, Seq, State) -> if @@ -498,10 +434,6 @@ should_start_job(#job{name = Name, seq = Seq, server = Pid}, State) -> true; A < TotalChannels -> case Name of - % st_index name has three elements - {_, _, hastings} -> - {ok, CurrentSeq} = hastings_index:await(Pid, 0), - (Seq - CurrentSeq) < Threshold; % View name has two elements. _ when IsView -> % Since seq is 0, couch_index:get_state/2 won't diff --git a/src/ken/test/ken_server_test.erl b/src/ken/test/ken_server_test.erl index ba2f12da8d..e2cfb2c8f1 100644 --- a/src/ken/test/ken_server_test.erl +++ b/src/ken/test/ken_server_test.erl @@ -79,7 +79,7 @@ start_server(Module, Config) -> stop_server(Key, Cfg) -> {Key, Pid} = lists:keyfind(Key, 1, Cfg), - MRef = erlang:monitor(process, Pid), + MRef = monitor(process, Pid), true = exit(Pid, kill), receive {'DOWN', MRef, _, _, _} -> ok diff --git a/src/mango/rebar.config.script b/src/mango/rebar.config.script index c3083b3ee4..497fd4305b 100644 --- a/src/mango/rebar.config.script +++ b/src/mango/rebar.config.script @@ -13,16 +13,10 @@ % the License. -HaveDreyfus = code:lib_dir(dreyfus) /= {error, bad_name}. +ErlOpts = case lists:keyfind(erl_opts, 1, CONFIG) of + {erl_opts, Opts} -> Opts; + false -> [] +end, -if not HaveDreyfus -> CONFIG; true -> - CurrOpts = case lists:keyfind(erl_opts, 1, CONFIG) of - {erl_opts, Opts} -> Opts; - false -> [] - end, - NewOpts = [ - {d, 'HAVE_DREYFUS'} - ] ++ CurrOpts, - lists:keystore(erl_opts, 1, CONFIG, {erl_opts, NewOpts}) -end. +lists:keystore(erl_opts, 1, CONFIG, {erl_opts, ErlOpts}). diff --git a/src/mango/requirements.txt b/src/mango/requirements.txt index 123824330e..c8e8cff379 100644 --- a/src/mango/requirements.txt +++ b/src/mango/requirements.txt @@ -1,5 +1,5 @@ # nose2 0.13.0, requests, hypothesis version are driven # by the minimum version for python on centos 8 currently nose2==0.13.0 -requests==2.27.1 +requests==2.32.4 hypothesis==6.31.6 diff --git a/src/mango/src/mango_cursor.erl b/src/mango/src/mango_cursor.erl index 3e9849a093..ee20eaabab 100644 --- a/src/mango/src/mango_cursor.erl +++ b/src/mango/src/mango_cursor.erl @@ -26,20 +26,12 @@ -include("mango.hrl"). -include("mango_cursor.hrl"). --ifdef(HAVE_DREYFUS). -define(CURSOR_MODULES, [ mango_cursor_view, mango_cursor_text, mango_cursor_nouveau, mango_cursor_special ]). --else. --define(CURSOR_MODULES, [ - mango_cursor_view, - mango_cursor_nouveau, - mango_cursor_special -]). --endif. -define(SUPERVISOR, mango_cursor_sup). diff --git a/src/mango/src/mango_cursor_nouveau.erl b/src/mango/src/mango_cursor_nouveau.erl index ea9b1640d6..629c91a53a 100644 --- a/src/mango/src/mango_cursor_nouveau.erl +++ b/src/mango/src/mango_cursor_nouveau.erl @@ -49,7 +49,7 @@ create(Db, {Indexes, Trace}, Selector, Opts) -> end, NouveauLimit = get_nouveau_limit(), - Limit = erlang:min(NouveauLimit, couch_util:get_value(limit, Opts, mango_opts:default_limit())), + Limit = min(NouveauLimit, couch_util:get_value(limit, Opts, mango_opts:default_limit())), Skip = couch_util:get_value(skip, Opts, 0), Fields = couch_util:get_value(fields, Opts, all_fields), @@ -297,7 +297,7 @@ update_query_args(CAcc) -> }. get_limit(CAcc) -> - erlang:min(get_nouveau_limit(), CAcc#cacc.limit + CAcc#cacc.skip). + min(get_nouveau_limit(), CAcc#cacc.limit + CAcc#cacc.skip). get_nouveau_limit() -> config:get_integer("nouveau", "max_limit", 200). diff --git a/src/mango/src/mango_cursor_text.erl b/src/mango/src/mango_cursor_text.erl index 1a79b59761..764d32d845 100644 --- a/src/mango/src/mango_cursor_text.erl +++ b/src/mango/src/mango_cursor_text.erl @@ -12,8 +12,6 @@ -module(mango_cursor_text). --ifdef(HAVE_DREYFUS). - -export([ create/4, explain/1, @@ -55,7 +53,7 @@ create(Db, {Indexes, Trace}, Selector, Opts0) -> Stats = mango_execution_stats:stats_init(DbName), DreyfusLimit = get_dreyfus_limit(), - Limit = erlang:min(DreyfusLimit, couch_util:get_value(limit, Opts, mango_opts:default_limit())), + Limit = min(DreyfusLimit, couch_util:get_value(limit, Opts, mango_opts:default_limit())), Skip = couch_util:get_value(skip, Opts, 0), Fields = couch_util:get_value(fields, Opts, all_fields), @@ -327,7 +325,7 @@ update_query_args(CAcc) -> }. get_limit(CAcc) -> - erlang:min(get_dreyfus_limit(), CAcc#cacc.limit + CAcc#cacc.skip). + min(get_dreyfus_limit(), CAcc#cacc.limit + CAcc#cacc.skip). get_dreyfus_limit() -> config:get_integer("dreyfus", "max_limit", 200). @@ -1226,5 +1224,3 @@ t_explain(_) -> ?assertEqual(Response, explain(Cursor)). -endif. - --endif. diff --git a/src/mango/src/mango_doc.erl b/src/mango/src/mango_doc.erl index f8cb4c63bc..255debf9e6 100644 --- a/src/mango/src/mango_doc.erl +++ b/src/mango/src/mango_doc.erl @@ -399,7 +399,7 @@ get_field({Props}, [Name | Rest], Validator) -> get_field(Values, [Name | Rest], Validator) when is_list(Values) -> % Name might be an integer index into an array try - Pos = list_to_integer(binary_to_list(Name)), + Pos = binary_to_integer(Name), case Pos >= 0 andalso Pos < length(Values) of true -> % +1 because Erlang uses 1 based list indices @@ -441,7 +441,7 @@ rem_field({Props}, [Name | Rest]) -> rem_field(Values, [Name]) when is_list(Values) -> % Name might be an integer index into an array try - Pos = list_to_integer(binary_to_list(Name)), + Pos = binary_to_integer(Name), case Pos >= 0 andalso Pos < length(Values) of true -> % +1 because Erlang uses 1 based list indices @@ -456,7 +456,7 @@ rem_field(Values, [Name]) when is_list(Values) -> rem_field(Values, [Name | Rest]) when is_list(Values) -> % Name might be an integer index into an array try - Pos = list_to_integer(binary_to_list(Name)), + Pos = binary_to_integer(Name), case Pos >= 0 andalso Pos < length(Values) of true -> % +1 because Erlang uses 1 based list indices @@ -494,7 +494,7 @@ set_field({Props}, [Name | Rest], Value) -> set_field(Values, [Name], Value) when is_list(Values) -> % Name might be an integer index into an array try - Pos = list_to_integer(binary_to_list(Name)), + Pos = binary_to_integer(Name), case Pos >= 0 andalso Pos < length(Values) of true -> % +1 because Erlang uses 1 based list indices @@ -509,7 +509,7 @@ set_field(Values, [Name], Value) when is_list(Values) -> set_field(Values, [Name | Rest], Value) when is_list(Values) -> % Name might be an integer index into an array try - Pos = list_to_integer(binary_to_list(Name)), + Pos = binary_to_integer(Name), case Pos >= 0 andalso Pos < length(Values) of true -> % +1 because Erlang uses 1 based list indices diff --git a/src/mango/src/mango_idx_special.erl b/src/mango/src/mango_idx_special.erl index caa85edba4..ea3beda9c4 100644 --- a/src/mango/src/mango_idx_special.erl +++ b/src/mango/src/mango_idx_special.erl @@ -28,16 +28,16 @@ -include("mango.hrl"). validate(_) -> - erlang:exit(invalid_call). + exit(invalid_call). add(_, _) -> - erlang:exit(invalid_call). + exit(invalid_call). remove(_, _) -> - erlang:exit(invalid_call). + exit(invalid_call). from_ddoc(_) -> - erlang:exit(invalid_call). + exit(invalid_call). to_json(#idx{def = all_docs}) -> {[ diff --git a/src/mango/src/mango_json.erl b/src/mango/src/mango_json.erl index ca18d88982..36815feca1 100644 --- a/src/mango/src/mango_json.erl +++ b/src/mango/src/mango_json.erl @@ -105,7 +105,7 @@ to_binary(true) -> to_binary(false) -> false; to_binary(Data) when is_atom(Data) -> - list_to_binary(atom_to_list(Data)); + atom_to_binary(Data); to_binary(Data) when is_number(Data) -> Data; to_binary(Data) when is_binary(Data) -> diff --git a/src/mango/src/mango_native_proc.erl b/src/mango/src/mango_native_proc.erl index f7f6156eae..511a987199 100644 --- a/src/mango/src/mango_native_proc.erl +++ b/src/mango/src/mango_native_proc.erl @@ -98,7 +98,7 @@ handle_call(Msg, _From, St) -> {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}. handle_cast(garbage_collect, St) -> - erlang:garbage_collect(), + garbage_collect(), {noreply, St}; handle_cast(stop, St) -> {stop, normal, St}; diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl index 42031b7569..9c5b7a96f7 100644 --- a/src/mango/src/mango_selector.erl +++ b/src/mango/src/mango_selector.erl @@ -563,7 +563,7 @@ match({[{Field, Cond}]}, Value, Cmp) -> match(Cond, SubValue, Cmp) end; match({[_, _ | _] = _Props} = Sel, _Value, _Cmp) -> - erlang:error({unnormalized_selector, Sel}). + error({unnormalized_selector, Sel}). % Returns true if Selector requires all % fields in RequiredFields to exist in any matching documents. diff --git a/src/mango/src/mango_selector_text.erl b/src/mango/src/mango_selector_text.erl index 97e72061c6..fc30a57b61 100644 --- a/src/mango/src/mango_selector_text.erl +++ b/src/mango/src/mango_selector_text.erl @@ -214,7 +214,7 @@ convert(Path, Val) when is_binary(Val); is_number(Val); is_boolean(Val) -> {op_field, {make_field(Path, Val), value_str(Val)}}; % Anything else is a bad selector. convert(_Path, {Props} = Sel) when length(Props) > 1 -> - erlang:error({unnormalized_selector, Sel}). + error({unnormalized_selector, Sel}). to_query_nested(Args) -> QueryArgs = lists:map(fun to_query/1, Args), @@ -369,9 +369,9 @@ value_str(Value) when is_binary(Value) -> <<"\"", Escaped/binary, "\"">> end; value_str(Value) when is_integer(Value) -> - list_to_binary(integer_to_list(Value)); + integer_to_binary(Value); value_str(Value) when is_float(Value) -> - list_to_binary(float_to_list(Value)); + float_to_binary(Value); value_str(true) -> <<"true">>; value_str(false) -> @@ -427,7 +427,7 @@ replace_array_indexes([], NewPartsAcc, HasIntAcc) -> replace_array_indexes([Part | Rest], NewPartsAcc, HasIntAcc) -> {NewPart, HasInt} = try - _ = list_to_integer(binary_to_list(Part)), + _ = binary_to_integer(Part), {<<"[]">>, true} catch _:_ -> diff --git a/src/mango/src/mango_util.erl b/src/mango/src/mango_util.erl index 32d75000b5..837cbf3dbe 100644 --- a/src/mango/src/mango_util.erl +++ b/src/mango/src/mango_util.erl @@ -114,32 +114,32 @@ load_ddoc(Db, DDocId, DbOpts) -> end. defer(Mod, Fun, Args) -> - {Pid, Ref} = erlang:spawn_monitor(?MODULE, do_defer, [Mod, Fun, Args]), + {Pid, Ref} = spawn_monitor(?MODULE, do_defer, [Mod, Fun, Args]), receive {'DOWN', Ref, process, Pid, {mango_defer_ok, Value}} -> Value; {'DOWN', Ref, process, Pid, {mango_defer_throw, Value}} -> erlang:throw(Value); {'DOWN', Ref, process, Pid, {mango_defer_error, Value}} -> - erlang:error(Value); + error(Value); {'DOWN', Ref, process, Pid, {mango_defer_exit, Value}} -> - erlang:exit(Value) + exit(Value) end. do_defer(Mod, Fun, Args) -> - try erlang:apply(Mod, Fun, Args) of + try apply(Mod, Fun, Args) of Resp -> - erlang:exit({mango_defer_ok, Resp}) + exit({mango_defer_ok, Resp}) catch throw:Error:Stack -> couch_log:error("Defered error: ~w~n ~p", [{throw, Error}, Stack]), - erlang:exit({mango_defer_throw, Error}); + exit({mango_defer_throw, Error}); error:Error:Stack -> couch_log:error("Defered error: ~w~n ~p", [{error, Error}, Stack]), - erlang:exit({mango_defer_error, Error}); + exit({mango_defer_error, Error}); exit:Error:Stack -> couch_log:error("Defered error: ~w~n ~p", [{exit, Error}, Stack]), - erlang:exit({mango_defer_exit, Error}) + exit({mango_defer_exit, Error}) end. assert_ejson({Props}) -> diff --git a/src/mango/test/02-basic-find-test.py b/src/mango/test/02-basic-find-test.py index 9a701b06db..cae772b8a8 100644 --- a/src/mango/test/02-basic-find-test.py +++ b/src/mango/test/02-basic-find-test.py @@ -139,7 +139,7 @@ def test_multi_cond_duplicate_field(self): '"location.city":{"$exists":true}}}' ) r = self.db.sess.post(self.db.path("_find"), data=body) - r.raise_for_status() + mango.raise_for_status(r) docs = r.json()["docs"] # expectation is that only the second instance diff --git a/src/mango/test/04-key-tests.py b/src/mango/test/04-key-tests.py index 998f17ee52..1934d4266e 100644 --- a/src/mango/test/04-key-tests.py +++ b/src/mango/test/04-key-tests.py @@ -15,8 +15,9 @@ import unittest TEST_DOCS = [ - {"type": "complex_key", "title": "normal key"}, + {"_id": "100", "type": "complex_key", "title": "normal key"}, { + "_id": "200", "type": "complex_key", "title": "key with dot", "dot.key": "dot's value", @@ -24,14 +25,16 @@ "name.first": "Kvothe", }, { + "_id": "300", "type": "complex_key", "title": "key with peso", "$key": "peso", "deep": {"$key": "deep peso"}, "name": {"first": "Master Elodin"}, }, - {"type": "complex_key", "title": "unicode key", "": "apple"}, + {"_id": "400", "type": "complex_key", "title": "unicode key", "": "apple"}, { + "_id": "500", "title": "internal_fields_format", "utf8-1[]:string": "string", "utf8-2[]:boolean[]": True, diff --git a/src/mango/test/mango.py b/src/mango/test/mango.py index 066b5a3296..fea92f55ce 100644 --- a/src/mango/test/mango.py +++ b/src/mango/test/mango.py @@ -26,6 +26,19 @@ import limit_docs +class MangoException(requests.exceptions.HTTPError): + def __init__(self, err: requests.exceptions.HTTPError): + super().__init__(str(err), response=err.response) + self.request = err.request + self.response = err.response + + def __str__(self): + formatted = super().__str__() + request = "request: {}".format(self.request.body) + response = "response: {}".format(self.response.text) + return "\n".join([formatted, request, response]) + + COUCH_HOST = os.environ.get("COUCH_HOST") or "http://127.0.0.1:15984" COUCH_USER = os.environ.get("COUCH_USER") COUCH_PASS = os.environ.get("COUCH_PASS") @@ -43,9 +56,17 @@ def random_string(n_max): return "".join(random.choice(string.ascii_letters) for _ in range(n)) +def requests_session(): + # use trust_env=False to disable possible .netrc usage + sess = requests.session() + sess.trust_env = False + return sess + + def has_text_service(): - features = requests.get(COUCH_HOST).json()["features"] - return "search" in features + with requests_session() as sess: + features = sess.get(COUCH_HOST).json()["features"] + return "search" in features def clean_up_dbs(): @@ -58,6 +79,13 @@ def delay(n=5, t=0.5): time.sleep(t) +def raise_for_status(resp): + try: + resp.raise_for_status() + except requests.exceptions.HTTPError as err: + raise MangoException(err) from None + + class Concurrently(object): def __init__(self, thread, thread_args, start=True): self.thread = threading.Thread(target=self.wrapper, args=(thread, thread_args)) @@ -82,7 +110,7 @@ def join(self): class Database(object): def __init__(self, dbname): self.dbname = dbname - self.sess = requests.session() + self.sess = requests_session() self.sess.auth = (COUCH_USER, COUCH_PASS) self.sess.headers["Content-Type"] = "application/json" @@ -100,11 +128,11 @@ def create(self, q=1, n=1, partitioned=False): if r.status_code == 404: p = str(partitioned).lower() r = self.sess.put(self.url, params={"q": q, "n": n, "partitioned": p}) - r.raise_for_status() + raise_for_status(r) def delete(self): r = self.sess.delete(self.url) - r.raise_for_status() + raise_for_status(r) def recreate(self): r = self.sess.get(self.url) @@ -124,32 +152,32 @@ def save_doc(self, doc): def save_docs_with_conflicts(self, docs, **kwargs): body = json.dumps({"docs": docs, "new_edits": False}) r = self.sess.post(self.path("_bulk_docs"), data=body, params=kwargs) - r.raise_for_status() + raise_for_status(r) def save_docs(self, docs, **kwargs): for offset in range(0, len(docs), BULK_BATCH_SIZE): chunk = docs[offset : (offset + BULK_BATCH_SIZE)] body = {"docs": chunk} r = self.sess.post(self.path("_bulk_docs"), json=body, params=kwargs) - r.raise_for_status() + raise_for_status(r) for doc, result in zip(chunk, r.json()): doc["_id"] = result["id"] doc["_rev"] = result["rev"] def open_doc(self, docid): r = self.sess.get(self.path(docid)) - r.raise_for_status() + raise_for_status(r) return r.json() def delete_doc(self, docid): r = self.sess.get(self.path(docid)) - r.raise_for_status() + raise_for_status(r) original_rev = r.json()["_rev"] self.sess.delete(self.path(docid), params={"rev": original_rev}) def ddoc_info(self, ddocid): r = self.sess.get(self.path([ddocid, "_info"])) - r.raise_for_status() + raise_for_status(r) return r.json() def create_index( @@ -172,7 +200,7 @@ def create_index( body["index"]["partial_filter_selector"] = partial_filter_selector body = json.dumps(body) r = self.sess.post(self.path("_index"), data=body) - r.raise_for_status() + raise_for_status(r) assert r.json()["id"] is not None assert r.json()["name"] is not None @@ -215,7 +243,7 @@ def create_text_index( body["ddoc"] = ddoc body = json.dumps(body) r = self.sess.post(self.path("_index"), data=body) - r.raise_for_status() + raise_for_status(r) return r.json()["result"] == "created" def list_indexes(self, limit="", skip=""): @@ -224,7 +252,7 @@ def list_indexes(self, limit="", skip=""): if skip != "": skip = "skip=" + str(skip) r = self.sess.get(self.path("_index?" + limit + ";" + skip)) - r.raise_for_status() + raise_for_status(r) return r.json()["indexes"] def get_index(self, ddocid, name): @@ -247,7 +275,7 @@ def get_index(self, ddocid, name): def delete_index(self, ddocid, name, idx_type="json"): path = ["_index", ddocid, idx_type, name] r = self.sess.delete(self.path(path), params={"w": "3"}) - r.raise_for_status() + raise_for_status(r) while len(self.get_index(ddocid, name)) == 1: delay(t=0.1) @@ -306,7 +334,7 @@ def find( else: path = self.path("{}_find".format(ppath)) r = self.sess.post(path, data=body) - r.raise_for_status() + raise_for_status(r) if explain or return_raw: return r.json() else: diff --git a/src/mem3/include/mem3.hrl b/src/mem3/include/mem3.hrl index d97b254696..fa232e8405 100644 --- a/src/mem3/include/mem3.hrl +++ b/src/mem3/include/mem3.hrl @@ -22,7 +22,7 @@ dbname :: binary() | 'undefined', range :: [non_neg_integer() | '$1' | '$2'] | '_' | 'undefined', ref :: reference() | '_' | 'undefined', - opts :: list() | 'undefined' + opts = []:: list() | 'undefined' }). %% Do not reference outside of mem3. @@ -33,7 +33,7 @@ range :: [non_neg_integer() | '$1' | '$2'] | '_', ref :: reference() | 'undefined' | '_', order :: non_neg_integer() | 'undefined' | '_', - opts :: list() + opts = []:: list() }). %% types diff --git a/src/mem3/src/mem3.erl b/src/mem3/src/mem3.erl index c0d64d7e46..f748ff7a0a 100644 --- a/src/mem3/src/mem3.erl +++ b/src/mem3/src/mem3.erl @@ -18,6 +18,7 @@ restart/0, nodes/0, node_info/2, + props/1, shards/1, shards/2, choose_shards/2, n/1, n/2, @@ -38,9 +39,10 @@ -export([db_is_current/1]). -export([shard_creation_time/1]). -export([generate_shard_suffix/0]). +-export([get_db_doc/1, update_db_doc/1]). %% For mem3 use only. --export([name/1, node/1, range/1, engine/1]). +-export([name/1, node/1, range/1]). -include_lib("mem3/include/mem3.hrl"). @@ -72,7 +74,7 @@ restart() -> ]. compare_nodelists() -> Nodes = mem3:nodes(), - AllNodes = erlang:nodes([this, visible]), + AllNodes = nodes([this, visible]), {Replies, BadNodes} = gen_server:multi_call(Nodes, mem3_nodes, get_nodelist), Dict = lists:foldl( fun({Node, Nodelist}, D) -> @@ -115,6 +117,11 @@ nodes() -> node_info(Node, Key) -> mem3_nodes:get_node_info(Node, Key). +-spec props(DbName :: iodata()) -> []. +props(DbName) -> + Opts = mem3_shards:opts_for_db(DbName), + couch_util:get_value(props, Opts, []). + -spec shards(DbName :: iodata()) -> [#shard{}]. shards(DbName) -> shards_int(DbName, []). @@ -135,8 +142,7 @@ shards_int(DbName, Options) -> name = ShardDbName, dbname = ShardDbName, range = [0, (2 bsl 31) - 1], - order = undefined, - opts = [] + order = undefined } ]; ShardDbName -> @@ -147,8 +153,7 @@ shards_int(DbName, Options) -> node = config:node_name(), name = ShardDbName, dbname = ShardDbName, - range = [0, (2 bsl 31) - 1], - opts = [] + range = [0, (2 bsl 31) - 1] } ]; _ -> @@ -260,7 +265,7 @@ choose_shards(DbName, Nodes, Options) -> Suffix = couch_util:get_value(shard_suffix, Options, ""), N = mem3_util:n_val(couch_util:get_value(n, Options), NodeCount), if - N =:= 0 -> erlang:error(no_nodes_in_zone); + N =:= 0 -> error(no_nodes_in_zone); true -> ok end, Q = mem3_util:q_val( @@ -310,7 +315,7 @@ dbname(DbName) when is_list(DbName) -> dbname(DbName) when is_binary(DbName) -> DbName; dbname(_) -> - erlang:error(badarg). + error(badarg). %% @doc Determine if DocId belongs in shard (identified by record or filename) belongs(#shard{} = Shard, DocId) when is_binary(DocId) -> @@ -416,25 +421,13 @@ name(#ordered_shard{name = Name}) -> owner(DbName, DocId, Nodes) -> hd(mem3_util:rotate_list({DbName, DocId}, lists:usort(Nodes))). -engine(#shard{opts = Opts}) -> - engine(Opts); -engine(#ordered_shard{opts = Opts}) -> - engine(Opts); -engine(Opts) when is_list(Opts) -> - case couch_util:get_value(engine, Opts) of - Engine when is_binary(Engine) -> - [{engine, Engine}]; - _ -> - [] - end. - %% Check whether a node is up or down %% side effect: set up a connection to Node if there not yet is one. -spec ping(node()) -> pos_integer() | Error :: term(). ping(Node) -> - [{Node, Res}] = ping_nodes([Node]), + [{Node, Res}] = ping_nodes([Node], ?PING_TIMEOUT_IN_MS), Res. -spec ping(node(), Timeout :: pos_integer()) -> pos_integer() | Error :: term(). @@ -480,7 +473,7 @@ gather_ping_results(Refs, Until, Results) -> end; false -> Fun = fun(Ref, true, Acc) -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), Acc#{Ref => timeout} end, maps:fold(Fun, Results, Refs) @@ -507,7 +500,7 @@ do_ping(Node, Timeout) -> {Tag, Err} end. --spec dead_nodes() -> [node() | Error :: term()]. +-spec dead_nodes() -> [{node(), [node()]}]. %% @doc Returns a list of dead nodes from the cluster. %% @@ -525,32 +518,21 @@ do_ping(Node, Timeout) -> dead_nodes() -> dead_nodes(?PING_TIMEOUT_IN_MS). --spec dead_nodes(Timeout :: pos_integer()) -> [node() | Error :: term()]. +-spec dead_nodes(Timeout :: pos_integer()) -> [{node(), [node()]}]. dead_nodes(Timeout) when is_integer(Timeout), Timeout > 0 -> % Here we are trying to detect overlapping partitions where not all the % nodes connect to each other. For example: n1 connects to n2 and n3, but % n2 and n3 are not connected. - DeadFun = fun() -> - Expected = ordsets:from_list(mem3:nodes()), - Live = ordsets:from_list(mem3_util:live_nodes()), - Dead = ordsets:subtract(Expected, Live), - ordsets:to_list(Dead) + Nodes = [node() | erlang:nodes()], + Expected = erpc:multicall(Nodes, mem3, nodes, [], Timeout), + Live = erpc:multicall(Nodes, mem3_util, live_nodes, [], Timeout), + ZipF = fun + (N, {ok, E}, {ok, L}) -> {N, E -- L}; + (N, _, _) -> {N, Nodes} end, - {Responses, BadNodes} = multicall(DeadFun, Timeout), - AccF = lists:foldl( - fun - (Dead, Acc) when is_list(Dead) -> ordsets:union(Acc, Dead); - (Error, Acc) -> ordsets:union(Acc, [Error]) - end, - ordsets:from_list(BadNodes), - Responses - ), - ordsets:to_list(AccF). - -multicall(Fun, Timeout) when is_integer(Timeout), Timeout > 0 -> - F = fun() -> catch Fun() end, - rpc:multicall(erlang, apply, [F, []], Timeout). + DeadPerNode = lists:zipwith3(ZipF, Nodes, Expected, Live), + lists:sort([{N, lists:sort(D)} || {N, D} <- DeadPerNode, D =/= []]). db_is_current(#shard{name = Name}) -> db_is_current(Name); @@ -587,6 +569,12 @@ strip_shard_suffix(DbName) when is_binary(DbName) -> filename:rootname(DbName) end. +get_db_doc(DocId) -> + mem3_db_doc_updater:get_db_doc(DocId). + +update_db_doc(Doc) -> + mem3_db_doc_updater:update_db_doc(Doc). + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/mem3/src/mem3_db_doc_updater.erl b/src/mem3/src/mem3_db_doc_updater.erl new file mode 100644 index 0000000000..f5537f8c9d --- /dev/null +++ b/src/mem3/src/mem3_db_doc_updater.erl @@ -0,0 +1,107 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(mem3_db_doc_updater). + +-behaviour(gen_server). + +-export([ + get_db_doc/1, + update_db_doc/1, + + start_link/0, + + init/1, + handle_call/3, + handle_cast/2 +]). + +-include_lib("couch/include/couch_db.hrl"). + +% Early return shortcut +% +-define(THROW(RES), throw({reply, RES, nil})). + +get_db_doc(DocId) when is_binary(DocId) -> + Timeout = shard_update_timeout_msec(), + gen_server:call(first_node(), {get_db_doc, DocId}, Timeout). + +update_db_doc(#doc{} = Doc) -> + Timeout = shard_update_timeout_msec(), + gen_server:call(first_node(), {update_db_doc, Doc}, Timeout). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init(_) -> + {ok, nil}. + +handle_call({get_db_doc, DocId}, _From, nil = St) -> + {reply, get_db_doc_int(DocId), St}; +handle_call({update_db_doc, #doc{} = Doc}, _From, nil = St) -> + {reply, update_db_doc_int(Doc), St}; +handle_call(Msg, _From, nil = St) -> + {stop, {invalid_call, Msg}, invalid_call, St}. + +handle_cast(Msg, nil = St) -> + {stop, {invalid_cast, Msg}, St}. + +% Private + +update_db_doc_int(#doc{} = Doc) -> + ok = validate_coordinator(), + couch_util:with_db(mem3_sync:shards_db(), fun(Db) -> + try + Res = couch_db:update_doc(Db, Doc, [?ADMIN_CTX]), + ok = replicate_to_all_nodes(shard_update_timeout_msec()), + Res + catch + conflict -> + ?THROW({error, conflict}) + end + end). + +get_db_doc_int(DocId) -> + ok = validate_coordinator(), + ok = replicate_from_all_nodes(shard_update_timeout_msec()), + couch_util:with_db(mem3_sync:shards_db(), fun(Db) -> + case couch_db:open_doc(Db, DocId, [ejson_body]) of + {ok, #doc{deleted = true}} -> ?THROW({error, not_found}); + {ok, #doc{} = Doc} -> {ok, Doc}; + {not_found, _} -> ?THROW({error, not_found}) + end + end). + +validate_coordinator() -> + case hd(mem3_util:live_nodes()) =:= node() of + true -> ok; + false -> ?THROW({error, coordinator_changed}) + end. + +replicate_from_all_nodes(TimeoutMSec) -> + case mem3_util:replicate_dbs_from_all_nodes(TimeoutMSec) of + ok -> ok; + Error -> ?THROW({error, Error}) + end. + +replicate_to_all_nodes(TimeoutMSec) -> + case mem3_util:replicate_dbs_to_all_nodes(TimeoutMSec) of + ok -> ok; + Error -> ?THROW({error, Error}) + end. + +shard_update_timeout_msec() -> + config:get_integer("mem3", "shard_update_timeout_msec", 300000). + +first_node() -> + FirstNode = hd(mem3_util:live_nodes()), + {?MODULE, FirstNode}. diff --git a/src/mem3/src/mem3_hash.erl b/src/mem3/src/mem3_hash.erl index 6dfe3f45ae..a8119dd70e 100644 --- a/src/mem3/src/mem3_hash.erl +++ b/src/mem3/src/mem3_hash.erl @@ -23,33 +23,35 @@ -include_lib("mem3/include/mem3.hrl"). -include_lib("couch/include/couch_db.hrl"). -calculate(#shard{opts = Opts}, DocId) -> - Props = couch_util:get_value(props, Opts, []), - MFA = get_hash_fun_int(Props), +calculate(#shard{dbname = DbName}, DocId) -> + MFA = get_hash_fun(DbName), calculate(MFA, DocId); -calculate(#ordered_shard{opts = Opts}, DocId) -> - Props = couch_util:get_value(props, Opts, []), - MFA = get_hash_fun_int(Props), +calculate(#ordered_shard{dbname = DbName}, DocId) -> + MFA = get_hash_fun(DbName), calculate(MFA, DocId); calculate(DbName, DocId) when is_binary(DbName) -> MFA = get_hash_fun(DbName), calculate(MFA, DocId); +calculate(Props, DocId) when is_list(Props) -> + MFA = get_hash_fun(Props), + calculate(MFA, DocId); calculate({Mod, Fun, Args}, DocId) -> - erlang:apply(Mod, Fun, [DocId | Args]). + apply(Mod, Fun, [DocId | Args]). -get_hash_fun(#shard{opts = Opts}) -> - get_hash_fun_int(Opts); -get_hash_fun(#ordered_shard{opts = Opts}) -> - get_hash_fun_int(Opts); +get_hash_fun(#shard{dbname = DbName}) -> + get_hash_fun(DbName); +get_hash_fun(#ordered_shard{dbname = DbName}) -> + get_hash_fun(DbName); get_hash_fun(DbName0) when is_binary(DbName0) -> DbName = mem3:dbname(DbName0), try - [#shard{opts = Opts} | _] = mem3_shards:for_db(DbName), - get_hash_fun_int(couch_util:get_value(props, Opts, [])) + get_hash_fun_int(mem3:props(DbName)) catch error:database_does_not_exist -> {?MODULE, crc32, []} - end. + end; +get_hash_fun(Props) when is_list(Props) -> + get_hash_fun_int(Props). crc32(Item) when is_binary(Item) -> erlang:crc32(Item); diff --git a/src/mem3/src/mem3_httpd.erl b/src/mem3/src/mem3_httpd.erl index 745fe815ca..fb58bd8d1d 100644 --- a/src/mem3/src/mem3_httpd.erl +++ b/src/mem3/src/mem3_httpd.erl @@ -104,9 +104,7 @@ json_shards([], AccIn) -> List = dict:to_list(AccIn), {lists:sort(List)}; json_shards([#shard{node = Node, range = [B, E]} | Rest], AccIn) -> - HexBeg = couch_util:to_hex(<>), - HexEnd = couch_util:to_hex(<>), - Range = list_to_binary(HexBeg ++ "-" ++ HexEnd), + Range = mem3_util:range_to_hex([B, E]), json_shards(Rest, dict:append(Range, Node, AccIn)). sync_shard(ShardName) -> diff --git a/src/mem3/src/mem3_nodes.erl b/src/mem3/src/mem3_nodes.erl index 47bcd9cc64..334128456b 100644 --- a/src/mem3/src/mem3_nodes.erl +++ b/src/mem3/src/mem3_nodes.erl @@ -66,7 +66,7 @@ handle_call({get_node_info, Node, Key}, _From, State) -> {reply, Resp, State}; handle_call({add_node, Node, NodeInfo}, _From, State) -> gen_event:notify(mem3_events, {add_node, Node}), - ets:insert(?MODULE, {Node, NodeInfo}), + update_ets(Node, NodeInfo), {reply, ok, State}; handle_call({remove_node, Node}, _From, State) -> gen_event:notify(mem3_events, {remove_node, Node}), @@ -95,12 +95,26 @@ handle_info(_Info, State) -> {noreply, State}. %% internal functions - initialize_nodelist() -> DbName = mem3_sync:nodes_db(), {ok, Db} = mem3_util:ensure_exists(DbName), {ok, _} = couch_db:fold_docs(Db, fun first_fold/2, Db, []), + insert_if_missing(Db, [config:node_name() | mem3_seeds:get_seeds()]), + + % when creating the document for the local node, populate + % the placement zone as defined by the COUCHDB_ZONE environment + % variable. This is an additional update on top of the first, + % empty document so that we don't create conflicting revisions + % between different nodes in the cluster when using a seedlist. + case os:getenv("COUCHDB_ZONE") of + false -> + % do not support unsetting a zone. + ok; + Zone -> + set_zone(DbName, config:node_name(), ?l2b(Zone)) + end, + Seq = couch_db:get_update_seq(Db), couch_db:close(Db), Seq. @@ -111,7 +125,7 @@ first_fold(#full_doc_info{deleted = true}, Acc) -> {ok, Acc}; first_fold(#full_doc_info{id = Id} = DocInfo, Db) -> {ok, #doc{body = {Props}}} = couch_db:open_doc(Db, DocInfo, [ejson_body]), - ets:insert(?MODULE, {mem3_util:to_atom(Id), Props}), + update_ets(mem3_util:to_atom(Id), Props), {ok, Db}. listen_for_changes(Since) -> @@ -156,7 +170,7 @@ insert_if_missing(Db, Nodes) -> [_] -> Acc; [] -> - ets:insert(?MODULE, {Node, []}), + update_ets(Node, []), [#doc{id = couch_util:to_binary(Node)} | Acc] end end, @@ -169,3 +183,42 @@ insert_if_missing(Db, Nodes) -> true -> {ok, []} end. + +-spec update_ets(Node :: term(), NodeInfo :: [tuple()]) -> true. +update_ets(Node, NodeInfo) -> + ets:insert(?MODULE, {Node, NodeInfo}). + +% sets the placement zone for the given node document. +-spec set_zone(DbName :: binary(), Node :: string() | binary(), Zone :: binary()) -> ok. +set_zone(DbName, Node, Zone) -> + {ok, Db} = couch_db:open(DbName, [sys_db, ?ADMIN_CTX]), + Props = get_from_db(Db, Node), + CurrentZone = couch_util:get_value(<<"zone">>, Props), + case CurrentZone of + Zone -> + ok; + _ -> + couch_log:info("Setting node zone attribute to ~s~n", [Zone]), + Props1 = couch_util:set_value(<<"zone">>, Props, Zone), + save_to_db(Db, Node, Props1) + end, + couch_db:close(Db), + ok. + +% get a node document from the system nodes db as a property list +-spec get_from_db(Db :: any(), Node :: string() | binary()) -> [tuple()]. +get_from_db(Db, Node) -> + Id = couch_util:to_binary(Node), + {ok, Doc} = couch_db:open_doc(Db, Id, [ejson_body]), + {Props} = couch_doc:to_json_obj(Doc, []), + Props. + +% save a node document (represented as a property list) +% to the system nodes db and update the ETS cache. +-spec save_to_db(Db :: any(), Node :: string() | binary(), Props :: [tuple()]) -> ok. +save_to_db(Db, Node, Props) -> + Doc = couch_doc:from_json_obj({Props}), + #doc{body = {NodeInfo}} = Doc, + {ok, _} = couch_db:update_doc(Db, Doc, []), + update_ets(Node, NodeInfo), + ok. diff --git a/src/mem3/src/mem3_rep.erl b/src/mem3/src/mem3_rep.erl index 3df07a9fdc..f01c12663f 100644 --- a/src/mem3/src/mem3_rep.erl +++ b/src/mem3/src/mem3_rep.erl @@ -217,20 +217,21 @@ verify_purge_checkpoint(DbName, Props) -> %% looking for our push replication history and choosing the %% largest source_seq that has a target_seq =< TgtSeq. find_source_seq(SrcDb, TgtNode, TgtUUIDPrefix, TgtSeq) -> + SrcDbName = couch_db:name(SrcDb), case find_repl_doc(SrcDb, TgtUUIDPrefix) of {ok, TgtUUID, Doc} -> SrcNode = atom_to_binary(config:node_name(), utf8), - find_source_seq_int(Doc, SrcNode, TgtNode, TgtUUID, TgtSeq); + find_source_seq_int(SrcDbName, Doc, SrcNode, TgtNode, TgtUUID, TgtSeq); {not_found, _} -> couch_log:warning( "~p find_source_seq repl doc not_found " "src_db: ~p, tgt_node: ~p, tgt_uuid_prefix: ~p, tgt_seq: ~p", - [?MODULE, SrcDb, TgtNode, TgtUUIDPrefix, TgtSeq] + [?MODULE, SrcDbName, TgtNode, TgtUUIDPrefix, TgtSeq] ), 0 end. -find_source_seq_int(#doc{body = {Props}}, SrcNode0, TgtNode0, TgtUUID, TgtSeq) -> +find_source_seq_int(SrcDbName, #doc{body = {Props}}, SrcNode0, TgtNode0, TgtUUID, TgtSeq) -> SrcNode = case is_atom(SrcNode0) of true -> atom_to_binary(SrcNode0, utf8); @@ -262,9 +263,9 @@ find_source_seq_int(#doc{body = {Props}}, SrcNode0, TgtNode0, TgtUUID, TgtSeq) - [] -> couch_log:warning( "~p find_source_seq_int nil useable history " - "src_node: ~p, tgt_node: ~p, tgt_uuid: ~p, tgt_seq: ~p, " + "src_db: ~s src_node: ~p, tgt_node: ~p, tgt_uuid: ~p, tgt_seq: ~p, " "src_history: ~p", - [?MODULE, SrcNode, TgtNode, TgtUUID, TgtSeq, SrcHistory] + [?MODULE, SrcDbName, SrcNode, TgtNode, TgtUUID, TgtSeq, SrcHistory] ), 0 end. @@ -434,7 +435,7 @@ push_purges(Db, BatchSize, SrcShard, Tgt, HashFun) -> couch_util:get_value(<<"purge_seq">>, Props); {not_found, _} -> Oldest = couch_db:get_oldest_purge_seq(Db), - erlang:max(0, Oldest - 1) + max(0, Oldest - 1) end, BelongsFun = fun(Id) when is_binary(Id) -> case TgtRange of @@ -954,31 +955,31 @@ find_source_seq_int_test_() -> t_unknown_node(_) -> ?assertEqual( - find_source_seq_int(doc_(), <<"foo">>, <<"bing">>, <<"bar_uuid">>, 10), + find_source_seq_int(<<"db">>, doc_(), <<"foo">>, <<"bing">>, <<"bar_uuid">>, 10), 0 ). t_unknown_uuid(_) -> ?assertEqual( - find_source_seq_int(doc_(), <<"foo">>, <<"bar">>, <<"teapot">>, 10), + find_source_seq_int(<<"db">>, doc_(), <<"foo">>, <<"bar">>, <<"teapot">>, 10), 0 ). t_ok(_) -> ?assertEqual( - find_source_seq_int(doc_(), <<"foo">>, <<"bar">>, <<"bar_uuid">>, 100), + find_source_seq_int(<<"db">>, doc_(), <<"foo">>, <<"bar">>, <<"bar_uuid">>, 100), 100 ). t_old_ok(_) -> ?assertEqual( - find_source_seq_int(doc_(), <<"foo">>, <<"bar">>, <<"bar_uuid">>, 84), + find_source_seq_int(<<"db">>, doc_(), <<"foo">>, <<"bar">>, <<"bar_uuid">>, 84), 50 ). t_different_node(_) -> ?assertEqual( - find_source_seq_int(doc_(), <<"foo2">>, <<"bar">>, <<"bar_uuid">>, 92), + find_source_seq_int(<<"db">>, doc_(), <<"foo2">>, <<"bar">>, <<"bar_uuid">>, 92), 31 ). diff --git a/src/mem3/src/mem3_reshard.erl b/src/mem3/src/mem3_reshard.erl index 78537ec38a..c67b74a9fe 100644 --- a/src/mem3/src/mem3_reshard.erl +++ b/src/mem3/src/mem3_reshard.erl @@ -512,7 +512,7 @@ kill_job_int(#job{pid = undefined} = Job) -> kill_job_int(#job{pid = Pid, ref = Ref} = Job) -> couch_log:info("~p kill_job_int ~p", [?MODULE, jobfmt(Job)]), demonitor(Ref, [flush]), - case erlang:is_process_alive(Pid) of + case is_process_alive(Pid) of true -> ok = mem3_reshard_job_sup:terminate_child(Pid); false -> @@ -799,7 +799,7 @@ db_exists(Name) -> -spec db_monitor(pid()) -> no_return(). db_monitor(Server) -> couch_log:notice("~p db monitor ~p starting", [?MODULE, self()]), - EvtRef = erlang:monitor(process, couch_event_server), + EvtRef = monitor(process, couch_event_server), couch_event:register_all(self()), db_monitor_loop(Server, EvtRef). diff --git a/src/mem3/src/mem3_reshard_dbdoc.erl b/src/mem3/src/mem3_reshard_dbdoc.erl index ffed94a8f5..e2f0d0c502 100644 --- a/src/mem3/src/mem3_reshard_dbdoc.erl +++ b/src/mem3/src/mem3_reshard_dbdoc.erl @@ -12,129 +12,48 @@ -module(mem3_reshard_dbdoc). --behaviour(gen_server). - -export([ - update_shard_map/1, - - start_link/0, - - init/1, - handle_call/3, - handle_cast/2, - handle_info/2 + update_shard_map/1 ]). -include_lib("couch/include/couch_db.hrl"). -include("mem3_reshard.hrl"). --spec update_shard_map(#job{}) -> no_return | ok. update_shard_map(#job{source = Source, target = Target} = Job) -> - Node = hd(mem3_util:live_nodes()), + DocId = mem3:dbname(Source#shard.name), JobStr = mem3_reshard_job:jobfmt(Job), - LogMsg1 = "~p : ~p calling update_shard_map node:~p", - couch_log:notice(LogMsg1, [?MODULE, JobStr, Node]), - ServerRef = {?MODULE, Node}, - CallArg = {update_shard_map, Source, Target}, - TimeoutMSec = shard_update_timeout_msec(), + LogMsg1 = "~p : ~p calling update_shard_map", + couch_log:notice(LogMsg1, [?MODULE, JobStr]), try - case gen_server:call(ServerRef, CallArg, TimeoutMSec) of - {ok, _} -> ok; - {error, CallError} -> throw({error, CallError}) + case mem3:get_db_doc(DocId) of + {ok, #doc{} = Doc} -> + #doc{body = Body} = Doc, + NewBody = update_shard_props(Body, Source, Target), + NewDoc = Doc#doc{body = NewBody}, + case mem3:update_db_doc(NewDoc) of + {ok, _} -> + ok; + {error, UpdateError} -> + exit(UpdateError) + end, + LogMsg2 = "~p : ~p update_shard_map returned", + couch_log:notice(LogMsg2, [?MODULE, JobStr]), + TimeoutMSec = shard_update_timeout_msec(), + UntilSec = mem3_reshard:now_sec() + (TimeoutMSec div 1000), + case wait_source_removed(Source, 5, UntilSec) of + true -> + ok; + false -> + exit(shard_update_did_not_propagate) + end; + Error -> + exit(Error) end catch _:Err -> exit(Err) - end, - LogMsg2 = "~p : ~p update_shard_map on node:~p returned", - couch_log:notice(LogMsg2, [?MODULE, JobStr, Node]), - UntilSec = mem3_reshard:now_sec() + (TimeoutMSec div 1000), - case wait_source_removed(Source, 5, UntilSec) of - true -> ok; - false -> exit(shard_update_did_not_propagate) - end. - --spec start_link() -> {ok, pid()} | ignore | {error, term()}. -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -init(_) -> - couch_log:notice("~p start init()", [?MODULE]), - {ok, nil}. - -handle_call({update_shard_map, Source, Target}, _From, State) -> - Res = - try - update_shard_map(Source, Target) - catch - throw:{error, Error} -> - {error, Error} - end, - {reply, Res, State}; -handle_call(Call, From, State) -> - couch_log:error("~p unknown call ~p from: ~p", [?MODULE, Call, From]), - {noreply, State}. - -handle_cast(Cast, State) -> - couch_log:error("~p unexpected cast ~p", [?MODULE, Cast]), - {noreply, State}. - -handle_info(Info, State) -> - couch_log:error("~p unexpected info ~p", [?MODULE, Info]), - {noreply, State}. - -% Private - -update_shard_map(Source, Target) -> - ok = validate_coordinator(), - ok = replicate_from_all_nodes(shard_update_timeout_msec()), - DocId = mem3:dbname(Source#shard.name), - OldDoc = - case mem3_util:open_db_doc(DocId) of - {ok, #doc{deleted = true}} -> - throw({error, missing_source}); - {ok, #doc{} = Doc} -> - Doc; - {not_found, deleted} -> - throw({error, missing_source}); - OpenErr -> - throw({error, {shard_doc_open_error, OpenErr}}) - end, - #doc{body = OldBody} = OldDoc, - NewBody = update_shard_props(OldBody, Source, Target), - {ok, _} = write_shard_doc(OldDoc, NewBody), - ok = replicate_to_all_nodes(shard_update_timeout_msec()), - {ok, NewBody}. - -validate_coordinator() -> - case hd(mem3_util:live_nodes()) =:= node() of - true -> ok; - false -> throw({error, coordinator_changed}) - end. - -replicate_from_all_nodes(TimeoutMSec) -> - case mem3_util:replicate_dbs_from_all_nodes(TimeoutMSec) of - ok -> ok; - Error -> throw({error, Error}) end. -replicate_to_all_nodes(TimeoutMSec) -> - case mem3_util:replicate_dbs_to_all_nodes(TimeoutMSec) of - ok -> ok; - Error -> throw({error, Error}) - end. - -write_shard_doc(#doc{id = Id} = Doc, Body) -> - UpdatedDoc = Doc#doc{body = Body}, - couch_util:with_db(mem3_sync:shards_db(), fun(Db) -> - try - {ok, _} = couch_db:update_doc(Db, UpdatedDoc, []) - catch - conflict -> - throw({error, {conflict, Id, Doc#doc.body, UpdatedDoc}}) - end - end). - update_shard_props({Props0}, #shard{} = Source, [#shard{} | _] = Targets) -> {ByNode0} = couch_util:get_value(<<"by_node">>, Props0, {[]}), ByNodeKV = {<<"by_node">>, {update_by_node(ByNode0, Source, Targets)}}, @@ -205,12 +124,10 @@ node_key(#shard{node = Node}) -> couch_util:to_binary(Node). range_key(#shard{range = [B, E]}) -> - BHex = couch_util:to_hex(<>), - EHex = couch_util:to_hex(<>), - list_to_binary([BHex, "-", EHex]). + mem3_util:range_to_hex([B, E]). shard_update_timeout_msec() -> - config:get_integer("reshard", "shard_upate_timeout_msec", 300000). + config:get_integer("reshard", "shard_update_timeout_msec", 300000). wait_source_removed(#shard{name = Name} = Source, SleepSec, UntilSec) -> case check_source_removed(Source) of diff --git a/src/mem3/src/mem3_reshard_index.erl b/src/mem3/src/mem3_reshard_index.erl index 41e225d221..61390588a9 100644 --- a/src/mem3/src/mem3_reshard_index.erl +++ b/src/mem3/src/mem3_reshard_index.erl @@ -21,7 +21,6 @@ -define(MRVIEW, mrview). -define(DREYFUS, dreyfus). --define(HASTINGS, hastings). -define(NOUVEAU, nouveau). -include_lib("mem3/include/mem3.hrl"). @@ -63,8 +62,7 @@ fabric_design_docs(DbName) -> indices(DbName, Doc) -> mrview_indices(DbName, Doc) ++ nouveau_indices(DbName, Doc) ++ - [dreyfus_indices(DbName, Doc) || has_app(dreyfus)] ++ - [hastings_indices(DbName, Doc) || has_app(hastings)]. + [dreyfus_indices(DbName, Doc) || has_app(dreyfus)]. mrview_indices(DbName, Doc) -> try @@ -101,7 +99,7 @@ nouveau_indices(DbName, Doc) -> dreyfus_indices(DbName, Doc) -> try - Indices = dreyfus_index:design_doc_to_indexes(Doc), + Indices = dreyfus_index:design_doc_to_indexes(DbName, Doc), [{?DREYFUS, DbName, Index} || Index <- Indices] catch Tag:Err -> @@ -110,17 +108,6 @@ dreyfus_indices(DbName, Doc) -> [] end. -hastings_indices(DbName, Doc) -> - try - Indices = hastings_index:design_doc_to_indexes(Doc), - [{?HASTINGS, DbName, Index} || Index <- Indices] - catch - Tag:Err -> - Msg = "~p couldn't get hasting indices ~p ~p ~p:~p", - couch_log:error(Msg, [?MODULE, DbName, Doc, Tag, Err]), - [] - end. - build_index({?MRVIEW, DbName, MRSt} = Ctx, Try) -> ioq:set_io_priority({reshard, DbName}), await_retry( @@ -138,13 +125,6 @@ build_index({?DREYFUS, DbName, DIndex} = Ctx, Try) -> fun dreyfus_index:await/2, Ctx, Try - ); -build_index({?HASTINGS, DbName, HIndex} = Ctx, Try) -> - await_retry( - hastings_index_manager:get_index(DbName, HIndex), - fun hastings_index:await/2, - Ctx, - Try ). await_retry({ok, Pid}, AwaitIndex, {_, DbName, _} = Ctx, Try) -> @@ -196,8 +176,6 @@ index_info({?MRVIEW, DbName, MRSt}) -> GroupName = couch_mrview_index:get(idx_name, MRSt), {DbName, GroupName}; index_info({?DREYFUS, DbName, Index}) -> - {DbName, Index}; -index_info({?HASTINGS, DbName, Index}) -> {DbName, Index}. has_app(App) -> diff --git a/src/mem3/src/mem3_reshard_job.erl b/src/mem3/src/mem3_reshard_job.erl index b8a18b1768..aaacea3c9d 100644 --- a/src/mem3/src/mem3_reshard_job.erl +++ b/src/mem3/src/mem3_reshard_job.erl @@ -180,7 +180,7 @@ set_start_state(#job{split_state = State} = Job) -> undefined -> Fmt1 = "~p recover : unknown state ~s", couch_log:error(Fmt1, [?MODULE, jobfmt(Job)]), - erlang:error({invalid_split_job_recover_state, Job}); + error({invalid_split_job_recover_state, Job}); StartState -> Job#job{split_state = StartState} end. @@ -371,7 +371,7 @@ parent() -> handle_unknown_msg(Job, When, RMsg) -> LogMsg = "~p ~s received an unknown message ~p when in ~s", couch_log:error(LogMsg, [?MODULE, jobfmt(Job), RMsg, When]), - erlang:error({invalid_split_job_message, Job#job.id, When, RMsg}). + error({invalid_split_job_message, Job#job.id, When, RMsg}). initial_copy(#job{} = Job) -> Pid = spawn_link(?MODULE, initial_copy_impl, [Job]), @@ -411,7 +411,7 @@ topoff_impl(#job{source = #shard{} = Source, target = Targets}) -> BatchSize = config:get_integer( "rexi", "shard_split_topoff_batch_size", ?INTERNAL_REP_BATCH_SIZE ), - TMap = maps:from_list([{R, T} || #shard{range = R} = T <- Targets]), + TMap = #{R => T || #shard{range = R} = T <- Targets}, Opts = [ {batch_size, BatchSize}, {batch_count, all}, @@ -552,7 +552,7 @@ check_state(#job{split_state = State} = Job) -> true -> Job; false -> - erlang:error({invalid_shard_split_state, State, Job}) + error({invalid_shard_split_state, State, Job}) end. create_artificial_mem3_rep_checkpoints(#job{} = Job, Seq) -> @@ -665,7 +665,7 @@ reset_target(#job{source = Source, target = Targets} = Job) -> % Should never get here but if we do crash and don't continue LogMsg = "~p : ~p target unexpectedly found in shard map ~p", couch_log:error(LogMsg, [?MODULE, jobfmt(Job), Name]), - erlang:error({target_present_in_shard_map, Name}); + error({target_present_in_shard_map, Name}); {true, false} -> LogMsg = "~p : ~p resetting ~p target", couch_log:warning(LogMsg, [?MODULE, jobfmt(Job), Name]), diff --git a/src/mem3/src/mem3_reshard_sup.erl b/src/mem3/src/mem3_reshard_sup.erl index 5a28359fbf..42da19f7c9 100644 --- a/src/mem3/src/mem3_reshard_sup.erl +++ b/src/mem3/src/mem3_reshard_sup.erl @@ -24,9 +24,6 @@ start_link() -> init(_Args) -> Children = [ - {mem3_reshard_dbdoc, {mem3_reshard_dbdoc, start_link, []}, permanent, infinity, worker, [ - mem3_reshard_dbdoc - ]}, {mem3_reshard_job_sup, {mem3_reshard_job_sup, start_link, []}, permanent, infinity, supervisor, [mem3_reshard_job_sup]}, {mem3_reshard, {mem3_reshard, start_link, []}, permanent, brutal_kill, worker, [ diff --git a/src/mem3/src/mem3_rpc.erl b/src/mem3/src/mem3_rpc.erl index 70fc797dad..a70f6e0362 100644 --- a/src/mem3/src/mem3_rpc.erl +++ b/src/mem3/src/mem3_rpc.erl @@ -197,7 +197,7 @@ load_purge_infos_rpc(DbName, SrcUUID, BatchSize) -> couch_util:get_value(<<"purge_seq">>, Props); {not_found, _} -> Oldest = couch_db:get_oldest_purge_seq(Db), - erlang:max(0, Oldest - 1) + max(0, Oldest - 1) end, FoldFun = fun({PSeq, UUID, Id, Revs}, {Count, Infos, _}) -> NewCount = Count + length(Revs), @@ -265,7 +265,7 @@ compare_rev_epochs([{_, SourceSeq} | _], []) -> SourceSeq; compare_rev_epochs([{_, SourceSeq} | _], [{_, TargetSeq} | _]) -> % The source was moved to a new location independently, take the minimum - erlang:min(SourceSeq, TargetSeq) - 1. + min(SourceSeq, TargetSeq) - 1. %% @doc This adds a new update sequence checkpoint to the replication %% history. Checkpoints are keyed by the source node so that we @@ -382,11 +382,11 @@ rexi_call(Node, MFA, Timeout) -> {Ref, {ok, Reply}} -> Reply; {Ref, Error} -> - erlang:error(Error); + error(Error); {rexi_DOWN, Mon, _, Reason} -> - erlang:error({rexi_DOWN, {Node, Reason}}) + error({rexi_DOWN, {Node, Reason}}) after Timeout -> - erlang:error(timeout) + error(timeout) end after rexi_monitor:stop(Mon) diff --git a/src/mem3/src/mem3_shards.erl b/src/mem3/src/mem3_shards.erl index d37539db42..ba31ffbeb9 100644 --- a/src/mem3/src/mem3_shards.erl +++ b/src/mem3/src/mem3_shards.erl @@ -39,6 +39,7 @@ -define(DBS, mem3_dbs). -define(SHARDS, mem3_shards). +-define(OPTS, mem3_opts). -define(ATIMES, mem3_atimes). -define(OPENERS, mem3_openers). -define(RELISTEN_DELAY, 5000). @@ -46,14 +47,19 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -opts_for_db(DbName0) -> +opts_for_db(DbName) when is_list(DbName) -> + opts_for_db(list_to_binary(DbName)); +opts_for_db(DbName0) when is_binary(DbName0) -> DbName = mem3:dbname(DbName0), - {ok, Db} = mem3_util:ensure_exists(mem3_sync:shards_db()), - case couch_db:open_doc(Db, DbName, [ejson_body]) of - {ok, #doc{body = {Props}}} -> - mem3_util:get_shard_opts(Props); - {not_found, _} -> - erlang:error(database_does_not_exist, [DbName]) + try ets:lookup(?OPTS, DbName) of + [] -> + load_opts_from_disk(DbName); + [{_, Opts}] -> + gen_server:cast(?MODULE, {cache_hit, DbName}), + Opts + catch + error:badarg -> + load_opts_from_disk(DbName) end. for_db(DbName) -> @@ -80,7 +86,7 @@ for_docid(DbName, DocId) -> for_docid(DbName, DocId, []). for_docid(DbName, DocId, Options) -> - HashKey = mem3_hash:calculate(DbName, DocId), + HashKey = mem3_hash:calculate(mem3:props(DbName), DocId), ShardHead = #shard{ dbname = DbName, range = ['$1', '$2'], @@ -97,13 +103,13 @@ for_docid(DbName, DocId, Options) -> Shards = try ets:select(?SHARDS, [ShardSpec, OrderedShardSpec]) of [] -> - load_shards_from_disk(DbName, DocId); + load_shards_from_disk(DbName, HashKey); Else -> gen_server:cast(?MODULE, {cache_hit, DbName}), Else catch error:badarg -> - load_shards_from_disk(DbName, DocId) + load_shards_from_disk(DbName, HashKey) end, case lists:member(ordered, Options) of true -> Shards; @@ -225,13 +231,9 @@ handle_config_terminate(_Server, _Reason, _State) -> init([]) -> couch_util:set_mqd_off_heap(?MODULE), - ets:new(?SHARDS, [ - bag, - public, - named_table, - {keypos, #shard.dbname}, - {read_concurrency, true} - ]), + CacheEtsOpts = [public, named_table, {read_concurrency, true}, {write_concurrency, auto}], + ets:new(?OPTS, CacheEtsOpts), + ets:new(?SHARDS, [bag, {keypos, #shard.dbname}] ++ CacheEtsOpts), ets:new(?DBS, [set, protected, named_table]), ets:new(?ATIMES, [ordered_set, protected, named_table]), ets:new(?OPENERS, [bag, public, named_table]), @@ -239,6 +241,7 @@ init([]) -> SizeList = config:get("mem3", "shard_cache_size", "25000"), WriteTimeout = config:get_integer("mem3", "shard_write_timeout", 1000), DbName = mem3_sync:shards_db(), + cache_shards_db_props(), ioq:set_io_priority({system, DbName}), UpdateSeq = get_update_seq(), {ok, #st{ @@ -304,6 +307,7 @@ handle_info({'DOWN', _, _, Pid, Reason}, #st{changes_pid = Pid} = St) -> couch_log:notice("~p changes listener died ~p", [?MODULE, Reason]), {St, get_update_seq()} end, + cache_shards_db_props(), erlang:send_after(5000, self(), {start_listener, Seq}), {noreply, NewSt#st{changes_pid = undefined}}; handle_info({start_listener, Seq}, St) -> @@ -324,9 +328,9 @@ terminate(_Reason, #st{changes_pid = Pid}) -> start_changes_listener(SinceSeq) -> Self = self(), - {Pid, _} = erlang:spawn_monitor(fun() -> - erlang:spawn_link(fun() -> - Ref = erlang:monitor(process, Self), + {Pid, _} = spawn_monitor(fun() -> + spawn_link(fun() -> + Ref = monitor(process, Self), receive {'DOWN', Ref, _, _, _} -> ok @@ -361,6 +365,7 @@ listen_for_changes(Since) -> DbName = mem3_sync:shards_db(), ioq:set_io_priority({system, DbName}), {ok, Db} = mem3_util:ensure_exists(DbName), + Args = #changes_args{ feed = "continuous", since = Since, @@ -393,10 +398,11 @@ changes_callback({change, {Change}, _}, _) -> ); {Doc} -> Shards = mem3_util:build_ordered_shards(DbName, Doc), + DbOpts = mem3_util:get_shard_opts(Doc), IdleTimeout = config:get_integer( "mem3", "writer_idle_timeout", 30000 ), - Writer = spawn_shard_writer(DbName, Shards, IdleTimeout), + Writer = spawn_shard_writer(DbName, DbOpts, Shards, IdleTimeout), ets:insert(?OPENERS, {DbName, Writer}), Msg = {cache_insert_change, DbName, Writer, Seq}, gen_server:cast(?MODULE, Msg), @@ -417,18 +423,30 @@ load_shards_from_disk(DbName) when is_binary(DbName) -> couch_stats:increment_counter([mem3, shard_cache, miss]), {ok, Db} = mem3_util:ensure_exists(mem3_sync:shards_db()), try - load_shards_from_db(Db, DbName) + {Shards, _DbOpts} = load_from_db(Db, DbName), + Shards after couch_db:close(Db) end. -load_shards_from_db(ShardDb, DbName) -> +load_opts_from_disk(DbName) when is_binary(DbName) -> + couch_stats:increment_counter([mem3, shard_cache, miss]), + {ok, Db} = mem3_util:ensure_exists(mem3_sync:shards_db()), + try + {_Shards, DbOpts} = load_from_db(Db, DbName), + DbOpts + after + couch_db:close(Db) + end. + +load_from_db(ShardDb, DbName) -> case couch_db:open_doc(ShardDb, DbName, [ejson_body]) of {ok, #doc{body = {Props}}} -> Seq = couch_db:get_update_seq(ShardDb), Shards = mem3_util:build_ordered_shards(DbName, Props), + DbOpts = mem3_util:get_shard_opts(Props), IdleTimeout = config:get_integer("mem3", "writer_idle_timeout", 30000), - case maybe_spawn_shard_writer(DbName, Shards, IdleTimeout) of + case maybe_spawn_shard_writer(DbName, DbOpts, Shards, IdleTimeout) of Writer when is_pid(Writer) -> case ets:insert_new(?OPENERS, {DbName, Writer}) of true -> @@ -440,14 +458,13 @@ load_shards_from_db(ShardDb, DbName) -> ignore -> ok end, - Shards; + {Shards, DbOpts}; {not_found, _} -> - erlang:error(database_does_not_exist, [DbName]) + error(database_does_not_exist, [DbName]) end. -load_shards_from_disk(DbName, DocId) -> +load_shards_from_disk(DbName, HashKey) -> Shards = load_shards_from_disk(DbName), - HashKey = mem3_hash:calculate(hd(Shards), DocId), [S || S <- Shards, in_range(S, HashKey)]. in_range(Shard, HashKey) -> @@ -474,6 +491,7 @@ create_if_missing(ShardName) -> cache_insert(#st{cur_size = Cur} = St, DbName, Writer, Timeout) -> NewATime = couch_util:unique_monotonic_integer(), true = ets:delete(?SHARDS, DbName), + true = ets:delete(?OPTS, DbName), flush_write(DbName, Writer, Timeout), case ets:lookup(?DBS, DbName) of [{DbName, ATime}] -> @@ -489,6 +507,7 @@ cache_insert(#st{cur_size = Cur} = St, DbName, Writer, Timeout) -> cache_remove(#st{cur_size = Cur} = St, DbName) -> true = ets:delete(?SHARDS, DbName), + true = ets:delete(?OPTS, DbName), case ets:lookup(?DBS, DbName) of [{DbName, ATime}] -> true = ets:delete(?DBS, DbName), @@ -515,6 +534,7 @@ cache_free(#st{max_size = Max, cur_size = Cur} = St) when Max =< Cur -> true = ets:delete(?ATIMES, ATime), true = ets:delete(?DBS, DbName), true = ets:delete(?SHARDS, DbName), + true = ets:delete(?OPTS, DbName), cache_free(St#st{cur_size = Cur - 1}); cache_free(St) -> St. @@ -522,24 +542,32 @@ cache_free(St) -> cache_clear(St) -> true = ets:delete_all_objects(?DBS), true = ets:delete_all_objects(?SHARDS), + true = ets:delete_all_objects(?OPTS), true = ets:delete_all_objects(?ATIMES), St#st{cur_size = 0}. -maybe_spawn_shard_writer(DbName, Shards, IdleTimeout) -> - case ets:member(?OPENERS, DbName) of +maybe_spawn_shard_writer(DbName, DbOpts, Shards, IdleTimeout) -> + try ets:member(?OPENERS, DbName) of true -> ignore; false -> - spawn_shard_writer(DbName, Shards, IdleTimeout) + spawn_shard_writer(DbName, DbOpts, Shards, IdleTimeout) + catch + error:badarg -> + % We might have been called before mem3 finished initializing + % from the error:badarg clause in for_db/2, for instance, so + % we shouldn't expect ?OPENERS to exist yet + ignore end. -spawn_shard_writer(DbName, Shards, IdleTimeout) -> - erlang:spawn(fun() -> shard_writer(DbName, Shards, IdleTimeout) end). +spawn_shard_writer(DbName, DbOpts, Shards, IdleTimeout) -> + spawn(fun() -> shard_writer(DbName, DbOpts, Shards, IdleTimeout) end). -shard_writer(DbName, Shards, IdleTimeout) -> +shard_writer(DbName, DbOpts, Shards, IdleTimeout) -> try receive write -> + true = ets:insert(?OPTS, {DbName, DbOpts}), true = ets:insert(?SHARDS, Shards); cancel -> ok @@ -551,15 +579,15 @@ shard_writer(DbName, Shards, IdleTimeout) -> end. flush_write(DbName, Writer, WriteTimeout) -> - Ref = erlang:monitor(process, Writer), + Ref = monitor(process, Writer), Writer ! write, receive {'DOWN', Ref, _, _, normal} -> ok; {'DOWN', Ref, _, _, Error} -> - erlang:exit({mem3_shards_bad_write, Error}) + exit({mem3_shards_bad_write, Error}) after WriteTimeout -> - erlang:exit({mem3_shards_write_timeout, DbName}) + exit({mem3_shards_write_timeout, DbName}) end. filter_shards_by_range(Range, Shards) -> @@ -571,9 +599,23 @@ filter_shards_by_range(Range, Shards) -> Shards ). +cache_shards_db_props() -> + DbName = mem3_sync:shards_db(), + {ok, Db} = mem3_util:ensure_exists(DbName), + try + DbProps = couch_db:get_props(Db), + DbOpts = [{props, DbProps}], + ets:insert(?OPTS, {DbName, DbOpts}) + catch + error:badarg -> + ok + after + couch_db:close(Db) + end. + -ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). +-include_lib("couch/include/couch_eunit.hrl"). -define(DB, <<"eunit_db_name">>). -define(INFINITY, 99999999). @@ -588,22 +630,23 @@ mem3_shards_test_() -> fun setup/0, fun teardown/1, [ - t_maybe_spawn_shard_writer_already_exists(), - t_maybe_spawn_shard_writer_new(), - t_flush_writer_exists_normal(), - t_flush_writer_times_out(), - t_flush_writer_crashes(), - t_writer_deletes_itself_when_done(), - t_writer_does_not_delete_other_writers_for_same_shard(), - t_spawn_writer_in_load_shards_from_db(), - t_cache_insert_takes_new_update(), - t_cache_insert_ignores_stale_update_and_kills_worker() + ?TDEF_FE(t_maybe_spawn_shard_writer_already_exists), + ?TDEF_FE(t_maybe_spawn_shard_writer_new), + ?TDEF_FE(t_flush_writer_exists_normal), + ?TDEF_FE(t_flush_writer_times_out), + ?TDEF_FE(t_flush_writer_crashes), + ?TDEF_FE(t_writer_deletes_itself_when_done), + ?TDEF_FE(t_writer_does_not_delete_other_writers_for_same_shard), + ?TDEF_FE(t_spawn_writer_in_load_shards_from_db), + ?TDEF_FE(t_cache_insert_takes_new_update), + ?TDEF_FE(t_cache_insert_ignores_stale_update_and_kills_worker) ] } }. setup_all() -> ets:new(?SHARDS, [bag, public, named_table, {keypos, #shard.dbname}]), + ets:new(?OPTS, [set, public, named_table, {read_concurrency, true}, {write_concurrency, auto}]), ets:new(?OPENERS, [bag, public, named_table]), ets:new(?DBS, [set, public, named_table]), ets:new(?ATIMES, [ordered_set, public, named_table]), @@ -615,142 +658,130 @@ teardown_all(_) -> ets:delete(?ATIMES), ets:delete(?DBS), ets:delete(?OPENERS), - ets:delete(?SHARDS). + ets:delete(?SHARDS), + ets:delete(?OPTS). setup() -> ets:delete_all_objects(?ATIMES), ets:delete_all_objects(?DBS), ets:delete_all_objects(?OPENERS), - ets:delete_all_objects(?SHARDS). + ets:delete_all_objects(?SHARDS), + ets:delete_all_objects(?OPTS). teardown(_) -> ok. -t_maybe_spawn_shard_writer_already_exists() -> - ?_test(begin - ets:insert(?OPENERS, {?DB, self()}), - Shards = mock_shards(), - WRes = maybe_spawn_shard_writer(?DB, Shards, ?INFINITY), - ?assertEqual(ignore, WRes) - end). - -t_maybe_spawn_shard_writer_new() -> - ?_test(begin - Shards = mock_shards(), - WPid = maybe_spawn_shard_writer(?DB, Shards, 1000), - WRef = erlang:monitor(process, WPid), - ?assert(is_pid(WPid)), - ?assert(is_process_alive(WPid)), - WPid ! write, - ?assertEqual(normal, wait_writer_result(WRef)), - ?assertEqual(Shards, ets:tab2list(?SHARDS)) - end). - -t_flush_writer_exists_normal() -> - ?_test(begin - Shards = mock_shards(), - WPid = spawn_link_mock_writer(?DB, Shards, ?INFINITY), - ?assertEqual(ok, flush_write(?DB, WPid, ?INFINITY)), - ?assertEqual(Shards, ets:tab2list(?SHARDS)) - end). - -t_flush_writer_times_out() -> - ?_test(begin - WPid = spawn(fun() -> - receive - will_never_receive_this -> ok - end - end), - Error = {mem3_shards_write_timeout, ?DB}, - ?assertExit(Error, flush_write(?DB, WPid, 100)), - exit(WPid, kill) - end). - -t_flush_writer_crashes() -> - ?_test(begin - WPid = spawn(fun() -> - receive - write -> exit('kapow!') - end - end), - Error = {mem3_shards_bad_write, 'kapow!'}, - ?assertExit(Error, flush_write(?DB, WPid, 1000)) - end). - -t_writer_deletes_itself_when_done() -> - ?_test(begin - Shards = mock_shards(), - WPid = spawn_link_mock_writer(?DB, Shards, ?INFINITY), - WRef = erlang:monitor(process, WPid), - ets:insert(?OPENERS, {?DB, WPid}), - WPid ! write, - ?assertEqual(normal, wait_writer_result(WRef)), - ?assertEqual(Shards, ets:tab2list(?SHARDS)), - ?assertEqual([], ets:tab2list(?OPENERS)) - end). - -t_writer_does_not_delete_other_writers_for_same_shard() -> - ?_test(begin - Shards = mock_shards(), - WPid = spawn_link_mock_writer(?DB, Shards, ?INFINITY), - WRef = erlang:monitor(process, WPid), - ets:insert(?OPENERS, {?DB, WPid}), - % should not be deleted - ets:insert(?OPENERS, {?DB, self()}), - WPid ! write, - ?assertEqual(normal, wait_writer_result(WRef)), - ?assertEqual(Shards, ets:tab2list(?SHARDS)), - ?assertEqual(1, ets:info(?OPENERS, size)), - ?assertEqual([{?DB, self()}], ets:tab2list(?OPENERS)) - end). - -t_spawn_writer_in_load_shards_from_db() -> - ?_test(begin - meck:expect(couch_db, open_doc, 3, {ok, #doc{body = {[]}}}), - meck:expect(couch_db, get_update_seq, 1, 1), - meck:expect(mem3_util, build_ordered_shards, 2, mock_shards()), - % register to get cache_insert cast - erlang:register(?MODULE, self()), - load_shards_from_db(test_util:fake_db([{name, <<"testdb">>}]), ?DB), - meck:validate(couch_db), - meck:validate(mem3_util), - Cast = - receive - {'$gen_cast', Msg} -> Msg - after 1000 -> - timeout - end, - ?assertMatch({cache_insert, ?DB, Pid, 1} when is_pid(Pid), Cast), - {cache_insert, _, WPid, _} = Cast, - exit(WPid, kill), - ?assertEqual([{?DB, WPid}], ets:tab2list(?OPENERS)), - meck:unload(couch_db), - meck:unload(mem3_util) - end). - -t_cache_insert_takes_new_update() -> - ?_test(begin - Shards = mock_shards(), - WPid = spawn_link_mock_writer(?DB, Shards, ?INFINITY), - Msg = {cache_insert, ?DB, WPid, 2}, - {noreply, NewState} = handle_cast(Msg, mock_state(1)), - ?assertMatch(#st{cur_size = 1}, NewState), - ?assertEqual(Shards, ets:tab2list(?SHARDS)), - ?assertEqual([], ets:tab2list(?OPENERS)) - end). - -t_cache_insert_ignores_stale_update_and_kills_worker() -> - ?_test(begin - Shards = mock_shards(), - WPid = spawn_link_mock_writer(?DB, Shards, ?INFINITY), - WRef = erlang:monitor(process, WPid), - Msg = {cache_insert, ?DB, WPid, 1}, - {noreply, NewState} = handle_cast(Msg, mock_state(2)), - ?assertEqual(normal, wait_writer_result(WRef)), - ?assertMatch(#st{cur_size = 0}, NewState), - ?assertEqual([], ets:tab2list(?SHARDS)), - ?assertEqual([], ets:tab2list(?OPENERS)) - end). +t_maybe_spawn_shard_writer_already_exists(_) -> + ets:insert(?OPENERS, {?DB, self()}), + Shards = mock_shards(), + WRes = maybe_spawn_shard_writer(?DB, [{x, y}], Shards, ?INFINITY), + ?assertEqual(ignore, WRes). + +t_maybe_spawn_shard_writer_new(_) -> + Shards = mock_shards(), + WPid = maybe_spawn_shard_writer(?DB, [{x, y}], Shards, 1000), + WRef = monitor(process, WPid), + ?assert(is_pid(WPid)), + ?assert(is_process_alive(WPid)), + WPid ! write, + ?assertEqual(normal, wait_writer_result(WRef)), + ?assertEqual(Shards, ets:tab2list(?SHARDS)), + ?assertEqual([{?DB, [{x, y}]}], ets:tab2list(?OPTS)). + +t_flush_writer_exists_normal(_) -> + Shards = mock_shards(), + WPid = spawn_link_mock_writer(?DB, [{x, y}], Shards, ?INFINITY), + ?assertEqual(ok, flush_write(?DB, WPid, ?INFINITY)), + ?assertEqual(Shards, ets:tab2list(?SHARDS)), + ?assertEqual([{?DB, [{x, y}]}], ets:tab2list(?OPTS)). + +t_flush_writer_times_out(_) -> + WPid = spawn(fun() -> + receive + will_never_receive_this -> ok + end + end), + Error = {mem3_shards_write_timeout, ?DB}, + ?assertExit(Error, flush_write(?DB, WPid, 100)), + exit(WPid, kill). + +t_flush_writer_crashes(_) -> + WPid = spawn(fun() -> + receive + write -> exit('kapow!') + end + end), + Error = {mem3_shards_bad_write, 'kapow!'}, + ?assertExit(Error, flush_write(?DB, WPid, 1000)). + +t_writer_deletes_itself_when_done(_) -> + Shards = mock_shards(), + WPid = spawn_link_mock_writer(?DB, [{x, y}], Shards, ?INFINITY), + WRef = monitor(process, WPid), + ets:insert(?OPENERS, {?DB, WPid}), + WPid ! write, + ?assertEqual(normal, wait_writer_result(WRef)), + ?assertEqual(Shards, ets:tab2list(?SHARDS)), + ?assertEqual([{?DB, [{x, y}]}], ets:tab2list(?OPTS)), + ?assertEqual([], ets:tab2list(?OPENERS)). + +t_writer_does_not_delete_other_writers_for_same_shard(_) -> + Shards = mock_shards(), + WPid = spawn_link_mock_writer(?DB, [{x, y}], Shards, ?INFINITY), + WRef = monitor(process, WPid), + ets:insert(?OPENERS, {?DB, WPid}), + % should not be deleted + ets:insert(?OPENERS, {?DB, self()}), + WPid ! write, + ?assertEqual(normal, wait_writer_result(WRef)), + ?assertEqual(Shards, ets:tab2list(?SHARDS)), + ?assertEqual([{?DB, [{x, y}]}], ets:tab2list(?OPTS)), + ?assertEqual(1, ets:info(?OPENERS, size)), + ?assertEqual([{?DB, self()}], ets:tab2list(?OPENERS)). + +t_spawn_writer_in_load_shards_from_db(_) -> + meck:expect(couch_db, open_doc, 3, {ok, #doc{body = {[]}}}), + meck:expect(couch_db, get_update_seq, 1, 1), + meck:expect(mem3_util, build_ordered_shards, 2, mock_shards()), + % register to get cache_insert cast + erlang:register(?MODULE, self()), + load_from_db(test_util:fake_db([{name, <<"testdb">>}]), ?DB), + meck:validate(couch_db), + meck:validate(mem3_util), + Cast = + receive + {'$gen_cast', Msg} -> Msg + after 1000 -> + timeout + end, + ?assertMatch({cache_insert, ?DB, Pid, 1} when is_pid(Pid), Cast), + {cache_insert, _, WPid, _} = Cast, + exit(WPid, kill), + ?assertEqual([{?DB, WPid}], ets:tab2list(?OPENERS)), + meck:unload(couch_db), + meck:unload(mem3_util). + +t_cache_insert_takes_new_update(_) -> + Shards = mock_shards(), + WPid = spawn_link_mock_writer(?DB, [{x, y}], Shards, ?INFINITY), + Msg = {cache_insert, ?DB, WPid, 2}, + {noreply, NewState} = handle_cast(Msg, mock_state(1)), + ?assertMatch(#st{cur_size = 1}, NewState), + ?assertEqual(Shards, ets:tab2list(?SHARDS)), + ?assertEqual([{?DB, [{x, y}]}], ets:tab2list(?OPTS)), + ?assertEqual([], ets:tab2list(?OPENERS)). + +t_cache_insert_ignores_stale_update_and_kills_worker(_) -> + Shards = mock_shards(), + WPid = spawn_link_mock_writer(?DB, [{x, y}], Shards, ?INFINITY), + WRef = monitor(process, WPid), + Msg = {cache_insert, ?DB, WPid, 1}, + {noreply, NewState} = handle_cast(Msg, mock_state(2)), + ?assertEqual(normal, wait_writer_result(WRef)), + ?assertMatch(#st{cur_size = 0}, NewState), + ?assertEqual([], ets:tab2list(?SHARDS)), + ?assertEqual([], ets:tab2list(?OPTS)), + ?assertEqual([], ets:tab2list(?OPENERS)). mock_state(UpdateSeq) -> #st{ @@ -778,8 +809,8 @@ wait_writer_result(WRef) -> timeout end. -spawn_link_mock_writer(Db, Shards, Timeout) -> - erlang:spawn_link(fun() -> shard_writer(Db, Shards, Timeout) end). +spawn_link_mock_writer(Db, DbOpts, Shards, Timeout) -> + spawn_link(fun() -> shard_writer(Db, DbOpts, Shards, Timeout) end). mem3_shards_changes_test_() -> { @@ -798,7 +829,7 @@ should_kill_changes_listener_on_shutdown() -> {ok, Pid} = ?MODULE:start_link(), {ok, ChangesPid} = get_changes_pid(), ?assert(is_process_alive(ChangesPid)), - true = erlang:unlink(Pid), + true = unlink(Pid), true = test_util:stop_sync_throw( ChangesPid, fun() -> exit(Pid, shutdown) end, wait_timeout ), diff --git a/src/mem3/src/mem3_sup.erl b/src/mem3/src/mem3_sup.erl index 862ef6b50b..dc8bd854ee 100644 --- a/src/mem3/src/mem3_sup.erl +++ b/src/mem3/src/mem3_sup.erl @@ -18,19 +18,40 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init(_Args) -> + % Some startup order constraints based on call dependencies: + % + % * mem3_events gen_event should be started before all the others + % + % * mem3_nodes gen_server is needed so everyone can call mem3:nodes() + % + % * mem3_sync_nodes needs to run before mem3_sync and + % mem3_sync_event_listener, so they can both can call + % mem3_sync_nodes:add/1 + % + % * mem3_distribution force connects nodes from mem3:nodes(), so start it + % before mem3_sync since mem3_sync:initial_sync/0 expects the connected + % nodes to be there when calling mem3_sync_nodes:add(nodes()) + % + % * mem3_sync_event_listener has to start after mem3_sync, so it can call + % mem3_sync:push/2 + % + % * mem3_seeds and mem3_reshard_sup can wait till the end, as they will + % spawn background work that can go on for a while: seeding system dbs + % from other nodes running resharding jobs + % Children = [ child(mem3_events), child(mem3_nodes), - child(mem3_distribution), - child(mem3_seeds), - % Order important? + child(mem3_shards), child(mem3_sync_nodes), + child(mem3_distribution), child(mem3_sync), - child(mem3_shards), child(mem3_sync_event_listener), + child(mem3_seeds), + child(mem3_db_doc_updater), child(mem3_reshard_sup) ], - {ok, {{one_for_one, 10, 1}, couch_epi:register_service(mem3_epi, Children)}}. + {ok, {{rest_for_one, 10, 1}, couch_epi:register_service(mem3_epi, Children)}}. child(mem3_events) -> MFA = {gen_event, start_link, [{local, mem3_events}]}, diff --git a/src/mem3/src/mem3_sync.erl b/src/mem3/src/mem3_sync.erl index 04e4d18893..67eb771816 100644 --- a/src/mem3/src/mem3_sync.erl +++ b/src/mem3/src/mem3_sync.erl @@ -162,7 +162,7 @@ handle_info({'EXIT', Active, Reason}, State) -> {pending_changes, Count} -> maybe_resubmit(State, Job#job{pid = nil, count = Count}); _ -> - case mem3:db_is_current(Job#job.name) of + case is_job_current(Job, nodes(), mem3:nodes()) of true -> timer:apply_after(5000, ?MODULE, push, [Job#job{pid = nil}]); false -> @@ -390,6 +390,14 @@ maybe_redirect(Node) -> list_to_existing_atom(Redirect) end. +% Check that the db exists and node is either connected or part of the cluster. +% +is_job_current(#job{name = Name, node = Node}, ConnectedNodes, Mem3Nodes) -> + DbCurrent = mem3:db_is_current(Name), + Connected = lists:member(Node, ConnectedNodes), + InMem3Nodes = lists:member(Node, Mem3Nodes), + DbCurrent andalso (Connected orelse InMem3Nodes). + -ifdef(TEST). -include_lib("couch/include/couch_eunit.hrl"). @@ -404,4 +412,37 @@ find_next_node_test() -> ?assertEqual(x, find_next_node(n, [n, x], [n, x])), ?assertEqual(a, find_next_node(n, [a, n, x], [a, n, y])). +is_job_current_test_() -> + { + foreach, + fun setup/0, + fun teardown/1, + [ + ?TDEF_FE(t_is_job_current) + ] + }. + +setup() -> + Ctx = test_util:start_couch(), + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]), + couch_db:close(Db), + {Ctx, DbName}. + +teardown({Ctx, DbName}) -> + ok = couch_server:delete(DbName, [?ADMIN_CTX]), + test_util:stop_couch(Ctx). + +t_is_job_current({_, DbName}) -> + Job = #job{name = DbName, node = n1}, + ?assert(is_job_current(Job, [], [n1])), + ?assert(is_job_current(Job, [n1], [])), + ?assert(is_job_current(Job, [n1], [n1])), + ?assertNot(is_job_current(Job, [n2], [])), + ?assertNot(is_job_current(Job, [], [n2])), + ?assertNot(is_job_current(Job, [], [])), + ?assertNot(is_job_current(Job, [n2], [n2])), + ?assertNot(is_job_current(Job#job{name = <<"x">>}, [n1], [n1])), + ?assertNot(is_job_current(Job#job{name = <<"x">>}, [], [])). + -endif. diff --git a/src/mem3/src/mem3_sync_event_listener.erl b/src/mem3/src/mem3_sync_event_listener.erl index 7f9b2d3b25..2e16fc2b61 100644 --- a/src/mem3/src/mem3_sync_event_listener.erl +++ b/src/mem3/src/mem3_sync_event_listener.erl @@ -240,7 +240,7 @@ teardown_all(_) -> setup() -> {ok, Pid} = ?MODULE:start_link(), - erlang:unlink(Pid), + unlink(Pid), wait_config_subscribed(Pid), Pid. @@ -300,7 +300,7 @@ should_terminate(Pid) -> EventMgr = whereis(config_event), EventMgrWasAlive = (catch is_process_alive(EventMgr)), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), RestartFun = fun() -> exit(EventMgr, kill) end, {_, _} = test_util:with_process_restart(config_event, RestartFun), diff --git a/src/mem3/src/mem3_sync_security.erl b/src/mem3/src/mem3_sync_security.erl index fc1726901b..f7df4c4017 100644 --- a/src/mem3/src/mem3_sync_security.erl +++ b/src/mem3/src/mem3_sync_security.erl @@ -20,7 +20,7 @@ maybe_sync(#shard{} = Src, #shard{} = Dst) -> case is_local(Src#shard.name) of false -> - erlang:spawn(?MODULE, maybe_sync_int, [Src, Dst]); + spawn(?MODULE, maybe_sync_int, [Src, Dst]); true -> ok end. diff --git a/src/mem3/src/mem3_util.erl b/src/mem3/src/mem3_util.erl index 520f54629d..f45ee4063d 100644 --- a/src/mem3/src/mem3_util.erl +++ b/src/mem3/src/mem3_util.erl @@ -29,8 +29,7 @@ ]). -export([get_or_create_db/2, get_or_create_db_int/2]). -export([is_deleted/1, rotate_list/2]). --export([get_shard_opts/1, get_engine_opt/1, get_props_opt/1]). --export([get_shard_props/1, find_dirty_shards/0]). +-export([get_shard_opts/1]). -export([ iso8601_timestamp/0, live_nodes/0, @@ -44,7 +43,8 @@ non_overlapping_shards/1, non_overlapping_shards/3, calculate_max_n/1, - calculate_max_n/3 + calculate_max_n/3, + range_to_hex/1 ]). %% do not use outside mem3. @@ -71,15 +71,7 @@ name_shard(#ordered_shard{dbname = DbName, range = Range} = Shard, Suffix) -> Shard#ordered_shard{name = ?l2b(Name)}. make_name(DbName, [B, E], Suffix) -> - [ - "shards/", - couch_util:to_hex(<>), - "-", - couch_util:to_hex(<>), - "/", - DbName, - Suffix - ]. + ["shards/", ?b2l(range_to_hex([B, E])), "/", DbName, Suffix]. create_partition_map(DbName, N, Q, Nodes) -> create_partition_map(DbName, N, Q, Nodes, ""). @@ -172,6 +164,12 @@ update_db_doc(DbName, #doc{id = Id, body = Body} = Doc, ShouldMutate) -> couch_db:close(Db) end. +-spec range_to_hex([non_neg_integer()]) -> binary(). +range_to_hex([B, E]) when is_integer(B), is_integer(E) -> + HexB = couch_util:to_hex(<>), + HexE = couch_util:to_hex(<>), + ?l2b(HexB ++ "-" ++ HexE). + delete_db_doc(DocId) -> gen_server:cast(mem3_shards, {cache_remove, DocId}), delete_db_doc(mem3_sync:shards_db(), DocId, true). @@ -231,8 +229,7 @@ build_shards_by_node(DbName, DocProps) -> #shard{ dbname = DbName, node = to_atom(Node), - range = [Beg, End], - opts = get_shard_opts(DocProps) + range = [Beg, End] }, Suffix ) @@ -258,8 +255,7 @@ build_shards_by_range(DbName, DocProps) -> dbname = DbName, node = to_atom(Node), range = [Beg, End], - order = Order, - opts = get_shard_opts(DocProps) + order = Order }, Suffix ) @@ -271,14 +267,14 @@ build_shards_by_range(DbName, DocProps) -> ). to_atom(Node) when is_binary(Node) -> - list_to_atom(binary_to_list(Node)); + binary_to_atom(Node); to_atom(Node) when is_atom(Node) -> Node. to_integer(N) when is_integer(N) -> N; to_integer(N) when is_binary(N) -> - list_to_integer(binary_to_list(N)); + binary_to_integer(N); to_integer(N) when is_list(N) -> list_to_integer(N). @@ -469,13 +465,15 @@ range_overlap([A, B], [X, Y]) when -> A =< Y andalso X =< B. -non_overlapping_shards(Shards) -> +non_overlapping_shards([]) -> + []; +non_overlapping_shards([_ | _] = Shards) -> {Start, End} = lists:foldl( fun(Shard, {Min, Max}) -> [B, E] = mem3:range(Shard), {min(B, Min), max(E, Max)} end, - {0, ?RING_END}, + {?RING_END, 0}, Shards ), non_overlapping_shards(Shards, Start, End). @@ -642,50 +640,6 @@ merge_opts(New, Old) -> New ). -get_shard_props(ShardName) -> - case couch_db:open_int(ShardName, []) of - {ok, Db} -> - Props = - case couch_db_engine:get_props(Db) of - undefined -> []; - Else -> Else - end, - %% We don't normally store the default engine name - EngineProps = - case couch_db_engine:get_engine(Db) of - couch_bt_engine -> - []; - EngineName -> - [{engine, EngineName}] - end, - [{props, Props} | EngineProps]; - {not_found, _} -> - not_found; - Else -> - Else - end. - -find_dirty_shards() -> - mem3_shards:fold( - fun(#shard{node = Node, name = Name, opts = Opts} = Shard, Acc) -> - case Opts of - [] -> - Acc; - [{props, []}] -> - Acc; - _ -> - Props = rpc:call(Node, ?MODULE, get_shard_props, [Name]), - case Props =:= Opts of - true -> - Acc; - false -> - [{Shard, Props} | Acc] - end - end - end, - [] - ). - -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -705,10 +659,11 @@ range_overlap_test_() -> ] ]. -non_overlapping_shards_test() -> +non_overlapping_shards_test_() -> [ ?_assertEqual(Res, non_overlapping_shards(Shards)) || {Shards, Res} <- [ + {[], []}, { [shard(0, ?RING_END)], [shard(0, ?RING_END)] @@ -719,7 +674,7 @@ non_overlapping_shards_test() -> }, { [shard(0, 1), shard(0, 1)], - [shard(0, 1)] + [shard(0, 1), shard(0, 1)] }, { [shard(0, 1), shard(3, 4)], @@ -731,15 +686,15 @@ non_overlapping_shards_test() -> }, { [shard(1, 2), shard(0, 1)], - [shard(0, 1), shard(1, 2)] + [] }, { [shard(0, 1), shard(0, 2), shard(2, 5), shard(3, 5)], - [shard(0, 2), shard(2, 5)] + [shard(0, 2), shard(3, 5)] }, { - [shard(0, 2), shard(4, 5), shard(1, 3)], - [] + [shard(1, 2), shard(3, 4), shard(1, 4), shard(5, 6)], + [shard(1, 4), shard(5, 6)] } ] ]. @@ -777,4 +732,8 @@ calculate_max_n_custom_range_test_() -> shard(Begin, End) -> #shard{range = [Begin, End]}. +range_to_hex_test() -> + Range = [2147483648, 4294967295], + ?assertEqual(<<"80000000-ffffffff">>, range_to_hex(Range)). + -endif. diff --git a/src/mem3/test/eunit/mem3_distribution_test.erl b/src/mem3/test/eunit/mem3_distribution_test.erl index 4bccc872b7..b04ff55679 100644 --- a/src/mem3/test/eunit/mem3_distribution_test.erl +++ b/src/mem3/test/eunit/mem3_distribution_test.erl @@ -136,11 +136,12 @@ ping_nodes_test(_) -> {n1, {nodedown, n1}}, {n2, {nodedown, n2}} ], - couch_debug:ping_nodes() + couch_debug:ping_live_cluster_nodes() ), ?assertEqual({nodedown, n3}, couch_debug:ping(n3, 100)). dead_nodes_test(_) -> meck:expect(mem3, nodes, 0, [n1, n2, n3]), meck:expect(mem3_util, live_nodes, 0, [n1, n2]), - ?assertEqual([n3], couch_debug:dead_nodes()). + Node = node(), + ?assertEqual([{Node, [n3]}], couch_debug:dead_nodes()). diff --git a/src/mem3/test/eunit/mem3_rep_test.erl b/src/mem3/test/eunit/mem3_rep_test.erl index 470fb208d1..814fd11b22 100644 --- a/src/mem3/test/eunit/mem3_rep_test.erl +++ b/src/mem3/test/eunit/mem3_rep_test.erl @@ -209,7 +209,7 @@ get_all_docs(DbName) -> get_all_docs(DbName, #mrargs{}). get_all_docs(DbName, #mrargs{} = QArgs0) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc( fun() -> Cb = fun @@ -235,11 +235,11 @@ to_map({[_ | _]} = EJson) -> jiffy:decode(jiffy:encode(EJson), [return_maps]). create_db(DbName, Opts) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:create_db(DbName, Opts) end, GL). delete_db(DbName) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:delete_db(DbName, [?ADMIN_CTX]) end, GL). create_local_db(DbName) -> @@ -259,7 +259,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {Pid, Ref} = spawn_monitor(fun() -> case GroupLeader of undefined -> ok; - _ -> erlang:group_leader(GroupLeader, self()) + _ -> group_leader(GroupLeader, self()) end, exit({with_proc_res, Fun()}) end), @@ -269,7 +269,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {'DOWN', Ref, process, Pid, Error} -> error(Error) after Timeout -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), exit(Pid, kill), error({with_proc_timeout, Fun, Timeout}) end. diff --git a/src/mem3/test/eunit/mem3_reshard_changes_feed_test.erl b/src/mem3/test/eunit/mem3_reshard_changes_feed_test.erl index 1f6f89f8a8..140b376358 100644 --- a/src/mem3/test/eunit/mem3_reshard_changes_feed_test.erl +++ b/src/mem3/test/eunit/mem3_reshard_changes_feed_test.erl @@ -232,7 +232,7 @@ continuous_feed_should_work_during_split(#{db1 := Db}) -> {'DOWN', UpdaterRef, process, UpdaterPid, normal} -> ok; {'DOWN', UpdaterRef, process, UpdaterPid, Error} -> - erlang:error( + error( {test_context_failed, [ {module, ?MODULE}, {line, ?LINE}, @@ -295,11 +295,11 @@ changes_callback({stop, EndSeq, _Pending}, Acc) -> %% common helpers from here create_db(DbName, Opts) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:create_db(DbName, Opts) end, GL). delete_db(DbName) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:delete_db(DbName, [?ADMIN_CTX]) end, GL). with_proc(Fun) -> @@ -312,7 +312,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {Pid, Ref} = spawn_monitor(fun() -> case GroupLeader of undefined -> ok; - _ -> erlang:group_leader(GroupLeader, self()) + _ -> group_leader(GroupLeader, self()) end, exit({with_proc_res, Fun()}) end), @@ -322,7 +322,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {'DOWN', Ref, process, Pid, Error} -> error(Error) after Timeout -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), exit(Pid, kill), error({with_proc_timeout, Fun, Timeout}) end. diff --git a/src/mem3/test/eunit/mem3_reshard_test.erl b/src/mem3/test/eunit/mem3_reshard_test.erl index 2579649f97..d6c3877382 100644 --- a/src/mem3/test/eunit/mem3_reshard_test.erl +++ b/src/mem3/test/eunit/mem3_reshard_test.erl @@ -22,17 +22,7 @@ -define(TIMEOUT, 60). setup() -> - HaveDreyfus = code:lib_dir(dreyfus) /= {error, bad_name}, - case HaveDreyfus of - false -> ok; - true -> mock_dreyfus_indices() - end, - - HaveHastings = code:lib_dir(hastings) /= {error, bad_name}, - case HaveHastings of - false -> ok; - true -> mock_hastings_indices() - end, + mock_dreyfus_indices(), {Db1, Db2} = {?tempdb(), ?tempdb()}, create_db(Db1, [{q, 1}, {n, 1}]), PartProps = [{partitioned, true}, {hash, [couch_partition, hash, []]}], @@ -276,42 +266,24 @@ update_docs_before_topoff1(#{db1 := Db}) -> indices_are_built(#{db1 := Db}) -> {timeout, ?TIMEOUT, ?_test(begin - HaveDreyfus = code:lib_dir(dreyfus) /= {error, bad_name}, - HaveHastings = code:lib_dir(hastings) /= {error, bad_name}, - - add_test_docs(Db, #{docs => 10, mrview => 2, search => 2, geo => 2}), + add_test_docs(Db, #{docs => 10, mrview => 2, search => 2}), [#shard{name = Shard}] = lists:sort(mem3:local_shards(Db)), {ok, JobId} = mem3_reshard:start_split_job(Shard), wait_state(JobId, completed), Shards1 = lists:sort(mem3:local_shards(Db)), ?assertEqual(2, length(Shards1)), MRViewGroupInfo = get_group_info(Db, <<"_design/mrview00000">>), - ?assertMatch(#{<<"update_seq">> := 32}, MRViewGroupInfo), - - HaveDreyfus = code:lib_dir(dreyfus) /= {error, bad_name}, - case HaveDreyfus of - false -> - ok; - true -> - % 4 because there are 2 indices and 2 target shards - ?assertEqual(4, meck:num_calls(dreyfus_index, await, 2)) - end, + ?assertMatch(#{<<"update_seq">> := 28}, MRViewGroupInfo), - HaveHastings = code:lib_dir(hastings) /= {error, bad_name}, - case HaveHastings of - false -> - ok; - true -> - % 4 because there are 2 indices and 2 target shards - ?assertEqual(4, meck:num_calls(hastings_index, await, 2)) - end + % 4 because there are 2 indices and 2 target shards + ?assertEqual(4, meck:num_calls(dreyfus_index, await, 2)) end)}. % This test that indices are built despite intermittent errors. indices_can_be_built_with_errors(#{db1 := Db}) -> {timeout, ?TIMEOUT, ?_test(begin - add_test_docs(Db, #{docs => 10, mrview => 2, search => 2, geo => 2}), + add_test_docs(Db, #{docs => 10, mrview => 2, search => 2}), [#shard{name = Shard}] = lists:sort(mem3:local_shards(Db)), meck:expect( couch_index_server, @@ -343,11 +315,11 @@ indices_can_be_built_with_errors(#{db1 := Db}) -> Shards1 = lists:sort(mem3:local_shards(Db)), ?assertEqual(2, length(Shards1)), MRViewGroupInfo = get_group_info(Db, <<"_design/mrview00000">>), - ?assertMatch(#{<<"update_seq">> := 32}, MRViewGroupInfo) + ?assertMatch(#{<<"update_seq">> := 28}, MRViewGroupInfo) end)}. mock_dreyfus_indices() -> - meck:expect(dreyfus_index, design_doc_to_indexes, fun(Doc) -> + meck:expect(dreyfus_index, design_doc_to_indexes, fun(_, Doc) -> #doc{body = {BodyProps}} = Doc, case couch_util:get_value(<<"indexes">>, BodyProps) of undefined -> @@ -359,19 +331,6 @@ mock_dreyfus_indices() -> meck:expect(dreyfus_index_manager, get_index, fun(_, _) -> {ok, pid} end), meck:expect(dreyfus_index, await, fun(_, _) -> {ok, indexpid, someseq} end). -mock_hastings_indices() -> - meck:expect(hastings_index, design_doc_to_indexes, fun(Doc) -> - #doc{body = {BodyProps}} = Doc, - case couch_util:get_value(<<"st_indexes">>, BodyProps) of - undefined -> - []; - {[_]} -> - [{hastings, <<"db">>, hastings_index1}] - end - end), - meck:expect(hastings_index_manager, get_index, fun(_, _) -> {ok, pid} end), - meck:expect(hastings_index, await, fun(_, _) -> {ok, someseq} end). - % Split partitioned database split_partitioned_db(#{db2 := Db}) -> {timeout, ?TIMEOUT, @@ -824,7 +783,7 @@ get_all_docs(DbName) -> get_all_docs(DbName, #mrargs{}). get_all_docs(DbName, #mrargs{} = QArgs0) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc( fun() -> Cb = fun @@ -886,11 +845,11 @@ to_map({[_ | _]} = EJson) -> jiffy:decode(jiffy:encode(EJson), [return_maps]). create_db(DbName, Opts) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:create_db(DbName, Opts) end, GL). delete_db(DbName) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:delete_db(DbName, [?ADMIN_CTX]) end, GL). with_proc(Fun) -> @@ -903,7 +862,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {Pid, Ref} = spawn_monitor(fun() -> case GroupLeader of undefined -> ok; - _ -> erlang:group_leader(GroupLeader, self()) + _ -> group_leader(GroupLeader, self()) end, exit({with_proc_res, Fun()}) end), @@ -913,7 +872,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {'DOWN', Ref, process, Pid, Error} -> error(Error) after Timeout -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), exit(Pid, kill), error({with_proc_timeout, Fun, Timeout}) end. @@ -924,7 +883,6 @@ add_test_docs(DbName, #{} = DocSpec) -> pdocs(maps:get(pdocs, DocSpec, #{})) ++ ddocs(mrview, maps:get(mrview, DocSpec, [])) ++ ddocs(search, maps:get(search, DocSpec, [])) ++ - ddocs(geo, maps:get(geo, DocSpec, [])) ++ ldocs(maps:get(local, DocSpec, [])), Res = update_docs(DbName, Docs), Docs1 = lists:map( @@ -1021,17 +979,6 @@ ddprop(mrview) -> ]}} ]}} ]; -ddprop(geo) -> - [ - {<<"st_indexes">>, - {[ - {<<"area">>, - {[ - {<<"analyzer">>, <<"standard">>}, - {<<"index">>, <<"function(d){if(d.g){st_index(d.g)}}">>} - ]}} - ]}} - ]; ddprop(search) -> [ {<<"indexes">>, diff --git a/src/mem3/test/eunit/mem3_shards_test.erl b/src/mem3/test/eunit/mem3_shards_test.erl index 6d2766fa22..14f4bc0846 100644 --- a/src/mem3/test/eunit/mem3_shards_test.erl +++ b/src/mem3/test/eunit/mem3_shards_test.erl @@ -49,7 +49,8 @@ mem3_shards_db_create_props_test_() -> fun setup/0, fun teardown/1, [ - fun partitioned_shards_recreated_properly/1 + ?TDEF_FE(partitioned_shards_recreated_properly, ?TIMEOUT), + ?TDEF_FE(update_props, ?TIMEOUT) ] } } @@ -61,33 +62,45 @@ mem3_shards_db_create_props_test_() -> % properties. % SEE: apache/couchdb#3631 partitioned_shards_recreated_properly(#{dbname := DbName, dbdoc := DbDoc}) -> - {timeout, ?TIMEOUT, - ?_test(begin - #doc{body = {Body0}} = DbDoc, - Body1 = [{<<"foo">>, <<"bar">>} | Body0], - Shards = [Shard | _] = lists:sort(mem3:shards(DbName)), - ShardName = Shard#shard.name, - ?assert(is_partitioned(Shards)), - ok = with_proc(fun() -> couch_server:delete(ShardName, []) end), - ?assertThrow({not_found, no_db_file}, is_partitioned(Shard)), - ok = mem3_util:update_db_doc(DbDoc#doc{body = {Body1}}), - Shards = - [Shard | _] = test_util:wait_value( - fun() -> - lists:sort(mem3:shards(DbName)) - end, - Shards - ), - ?assertEqual( - true, - test_util:wait_value( - fun() -> - catch is_partitioned(Shard) - end, - true - ) - ) - end)}. + #doc{body = {Body0}} = DbDoc, + Body1 = [{<<"foo">>, <<"bar">>} | Body0], + Shards = [Shard | _] = lists:sort(mem3:shards(DbName)), + ShardName = Shard#shard.name, + ?assert(is_partitioned(Shards)), + ok = with_proc(fun() -> couch_server:delete(ShardName, []) end), + ?assertThrow({not_found, no_db_file}, is_partitioned(Shard)), + ok = mem3_util:update_db_doc(DbDoc#doc{body = {Body1}}), + Shards = + [Shard | _] = test_util:wait_value( + fun() -> + lists:sort(mem3:shards(DbName)) + end, + Shards + ), + ?assertEqual( + true, + test_util:wait_value( + fun() -> + catch is_partitioned(Shard) + end, + true + ) + ). + +update_props(#{dbname := DbName, dbdoc := DbDoc}) -> + {ok, Doc} = mem3:get_db_doc(DbName), + ?assertEqual(DbDoc, Doc), + #doc{body = {Body0}} = Doc, + {Props} = couch_util:get_value(<<"props">>, Body0, {[]}), + Props1 = couch_util:set_value(<<"baz">>, Props, <<"bar">>), + Body1 = couch_util:set_value(<<"props">>, Body0, {Props1}), + ResUpdate = mem3:update_db_doc(Doc#doc{body = {Body1}}), + ?assertMatch({ok, _}, ResUpdate), + {ok, Doc2} = mem3:get_db_doc(DbName), + #doc{body = {Body2}} = Doc2, + {Props2} = couch_util:get_value(<<"props">>, Body2, {[]}), + ?assertEqual(<<"bar">>, couch_util:get_value(<<"baz">>, Props2)), + ?assertEqual({error, conflict}, mem3:update_db_doc(Doc#doc{body = {Body1}})). is_partitioned([#shard{} | _] = Shards) -> lists:all(fun is_partitioned/1, Shards); @@ -97,11 +110,11 @@ is_partitioned(Db) -> couch_db:is_partitioned(Db). create_db(DbName, Opts) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:create_db(DbName, Opts) end, GL). delete_db(DbName) -> - GL = erlang:group_leader(), + GL = group_leader(), with_proc(fun() -> fabric:delete_db(DbName, [?ADMIN_CTX]) end, GL). with_proc(Fun) -> @@ -114,7 +127,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {Pid, Ref} = spawn_monitor(fun() -> case GroupLeader of undefined -> ok; - _ -> erlang:group_leader(GroupLeader, self()) + _ -> group_leader(GroupLeader, self()) end, exit({with_proc_res, Fun()}) end), @@ -124,7 +137,7 @@ with_proc(Fun, GroupLeader, Timeout) -> {'DOWN', Ref, process, Pid, Error} -> error(Error) after Timeout -> - erlang:demonitor(Ref, [flush]), + demonitor(Ref, [flush]), exit(Pid, kill), error({with_proc_timeout, Fun, Timeout}) end. diff --git a/src/mem3/test/eunit/mem3_zone_test.erl b/src/mem3/test/eunit/mem3_zone_test.erl new file mode 100644 index 0000000000..55fef3342f --- /dev/null +++ b/src/mem3/test/eunit/mem3_zone_test.erl @@ -0,0 +1,77 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(mem3_zone_test). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +mem3_zone_test_() -> + { + foreach, + fun setup/0, + fun teardown/1, + [ + ?TDEF_FE(t_empty_zone), + ?TDEF_FE(t_set_zone_from_env), + ?TDEF_FE(t_set_zone_when_node_in_seedlist), + ?TDEF_FE(t_zone_already_set) + ] + }. + +assertZoneEqual(Expected) -> + [Node | _] = mem3:nodes(), + Actual = mem3:node_info(Node, <<"zone">>), + ?assertEqual(Expected, Actual). + +t_empty_zone(_) -> + ok = application:start(mem3), + assertZoneEqual(undefined). + +t_set_zone_from_env(_) -> + Zone = "zone1", + os:putenv("COUCHDB_ZONE", Zone), + ok = application:start(mem3), + assertZoneEqual(iolist_to_binary(Zone)). + +t_set_zone_when_node_in_seedlist(_) -> + CfgSeeds = "nonode@nohost", + config:set("cluster", "seedlist", CfgSeeds, false), + Zone = "zone1", + os:putenv("COUCHDB_ZONE", Zone), + ok = application:start(mem3), + assertZoneEqual(iolist_to_binary(Zone)). + +t_zone_already_set(_) -> + Zone = "zone1", + os:putenv("COUCHDB_ZONE", Zone), + ok = application:start(mem3), + application:stop(mem3), + ok = application:start(mem3), + assertZoneEqual(iolist_to_binary(Zone)). + +setup() -> + meck:new(mem3_seeds, [passthrough]), + meck:new(mem3_rpc, [passthrough]), + test_util:start_couch([rexi]). + +teardown(Ctx) -> + catch application:stop(mem3), + os:unsetenv("COUCHDB_ZONE"), + Filename = config:get("mem3", "nodes_db", "_nodes") ++ ".couch", + file:delete(filename:join([?BUILDDIR(), "tmp", "data", Filename])), + case config:get("couch_httpd_auth", "authentication_db") of + undefined -> ok; + DbName -> couch_server:delete(list_to_binary(DbName), []) + end, + meck:unload(), + test_util:stop_couch(Ctx). diff --git a/src/nouveau/src/nouveau.app.src b/src/nouveau/src/nouveau.app.src index 0828437c18..e8ea54915c 100644 --- a/src/nouveau/src/nouveau.app.src +++ b/src/nouveau/src/nouveau.app.src @@ -18,7 +18,7 @@ {vsn, git}, {applications, [ config, - ibrowse, + gun, kernel, stdlib, mem3, diff --git a/src/nouveau/src/nouveau_api.erl b/src/nouveau/src/nouveau_api.erl index b700524f75..cfc88af4f7 100644 --- a/src/nouveau/src/nouveau_api.erl +++ b/src/nouveau/src/nouveau_api.erl @@ -23,13 +23,12 @@ create_index/2, delete_path/1, delete_path/2, - delete_doc_async/5, - purge_doc/5, - update_doc_async/7, + delete_doc/4, + purge_doc/4, + update_doc/6, search/2, - set_purge_seq/4, - set_update_seq/4, - drain_async_responses/2, + set_purge_seq/3, + set_update_seq/3, jaxrs_error/2 ]). @@ -40,13 +39,13 @@ analyze(Text, Analyzer) when -> ReqBody = {[{<<"text">>, Text}, {<<"analyzer">>, Analyzer}]}, Resp = send_if_enabled( - nouveau_util:nouveau_url() ++ "/analyze", + "/analyze", [?JSON_CONTENT_TYPE], - post, + <<"POST">>, jiffy:encode(ReqBody) ), case Resp of - {ok, "200", _, RespBody} -> + {ok, 200, _, RespBody} -> Json = jiffy:decode(RespBody, [return_maps]), {ok, maps:get(<<"tokens">>, Json)}; {ok, StatusCode, _, RespBody} -> @@ -58,9 +57,9 @@ analyze(_, _) -> {error, {bad_request, <<"'text' and 'analyzer' fields must be non-empty strings">>}}. index_info(#index{} = Index) -> - Resp = send_if_enabled(index_url(Index), [], get), + Resp = send_if_enabled(index_path(Index), [], <<"GET">>), case Resp of - {ok, "200", _, RespBody} -> + {ok, 200, _, RespBody} -> {ok, jiffy:decode(RespBody, [return_maps])}; {ok, StatusCode, _, RespBody} -> {error, jaxrs_error(StatusCode, RespBody)}; @@ -70,10 +69,10 @@ index_info(#index{} = Index) -> create_index(#index{} = Index, IndexDefinition) -> Resp = send_if_enabled( - index_url(Index), [?JSON_CONTENT_TYPE], put, jiffy:encode(IndexDefinition) + index_path(Index), [?JSON_CONTENT_TYPE], <<"PUT">>, jiffy:encode(IndexDefinition) ), case Resp of - {ok, "204", _, _} -> + {ok, 200, _, _} -> ok; {ok, StatusCode, _, RespBody} -> {error, jaxrs_error(StatusCode, RespBody)}; @@ -88,10 +87,10 @@ delete_path(Path, Exclusions) when is_binary(Path), is_list(Exclusions) -> Resp = send_if_enabled( - index_path(Path), [?JSON_CONTENT_TYPE], delete, jiffy:encode(Exclusions) + index_path(Path), [?JSON_CONTENT_TYPE], <<"DELETE">>, jiffy:encode(Exclusions) ), case Resp of - {ok, "204", _, _} -> + {ok, 200, _, _} -> ok; {ok, StatusCode, _, RespBody} -> {error, jaxrs_error(StatusCode, RespBody)}; @@ -99,8 +98,7 @@ delete_path(Path, Exclusions) when send_error(Reason) end. -delete_doc_async(ConnPid, #index{} = Index, DocId, MatchSeq, UpdateSeq) when - is_pid(ConnPid), +delete_doc(#index{} = Index, DocId, MatchSeq, UpdateSeq) when is_binary(DocId), is_integer(MatchSeq), MatchSeq >= 0, @@ -108,19 +106,22 @@ delete_doc_async(ConnPid, #index{} = Index, DocId, MatchSeq, UpdateSeq) when UpdateSeq > 0 -> ReqBody = #{match_seq => MatchSeq, seq => UpdateSeq, purge => false}, - send_direct_if_enabled( - ConnPid, - doc_url(Index, DocId), + Resp = send_if_enabled( + doc_path(Index, DocId), [?JSON_CONTENT_TYPE], - delete, - jiffy:encode(ReqBody), - [ - {stream_to, self()} - ] - ). + <<"DELETE">>, + jiffy:encode(ReqBody) + ), + case Resp of + {ok, 200, _, _} -> + ok; + {ok, StatusCode, _, RespBody} -> + {error, jaxrs_error(StatusCode, RespBody)}; + {error, Reason} -> + send_error(Reason) + end. -purge_doc(ConnPid, #index{} = Index, DocId, MatchSeq, PurgeSeq) when - is_pid(ConnPid), +purge_doc(#index{} = Index, DocId, MatchSeq, PurgeSeq) when is_binary(DocId), is_integer(MatchSeq), MatchSeq >= 0, @@ -128,11 +129,11 @@ purge_doc(ConnPid, #index{} = Index, DocId, MatchSeq, PurgeSeq) when PurgeSeq > 0 -> ReqBody = #{match_seq => MatchSeq, seq => PurgeSeq, purge => true}, - Resp = send_direct_if_enabled( - ConnPid, doc_url(Index, DocId), [?JSON_CONTENT_TYPE], delete, jiffy:encode(ReqBody), [] + Resp = send_if_enabled( + doc_path(Index, DocId), [?JSON_CONTENT_TYPE], <<"DELETE">>, jiffy:encode(ReqBody) ), case Resp of - {ok, "204", _, _} -> + {ok, 200, _, _} -> ok; {ok, StatusCode, _, RespBody} -> {error, jaxrs_error(StatusCode, RespBody)}; @@ -140,8 +141,7 @@ purge_doc(ConnPid, #index{} = Index, DocId, MatchSeq, PurgeSeq) when send_error(Reason) end. -update_doc_async(ConnPid, #index{} = Index, DocId, MatchSeq, UpdateSeq, Partition, Fields) when - is_pid(ConnPid), +update_doc(#index{} = Index, DocId, MatchSeq, UpdateSeq, Partition, Fields) when is_binary(DocId), is_integer(MatchSeq), MatchSeq >= 0, @@ -156,25 +156,29 @@ update_doc_async(ConnPid, #index{} = Index, DocId, MatchSeq, UpdateSeq, Partitio partition => Partition, fields => Fields }, - send_direct_if_enabled( - ConnPid, - doc_url(Index, DocId), + Resp = send_if_enabled( + doc_path(Index, DocId), [?JSON_CONTENT_TYPE], - put, - jiffy:encode(ReqBody), - [ - {stream_to, self()} - ] - ). + <<"PUT">>, + jiffy:encode(ReqBody) + ), + case Resp of + {ok, 200, _, _} -> + ok; + {ok, StatusCode, _, RespBody} -> + {error, jaxrs_error(StatusCode, RespBody)}; + {error, Reason} -> + send_error(Reason) + end. search(#index{} = Index, QueryArgs) -> Resp = send_if_enabled( - search_url(Index), [?JSON_CONTENT_TYPE], post, jiffy:encode(QueryArgs) + search_path(Index), [?JSON_CONTENT_TYPE], <<"POST">>, jiffy:encode(QueryArgs) ), case Resp of - {ok, "200", _, RespBody} -> + {ok, 200, _, RespBody} -> {ok, jiffy:decode(RespBody, [return_maps])}; - {ok, "409", _, _} -> + {ok, 409, _, _} -> %% Index was not current enough. {error, stale_index}; {ok, StatusCode, _, RespBody} -> @@ -183,26 +187,26 @@ search(#index{} = Index, QueryArgs) -> send_error(Reason) end. -set_update_seq(ConnPid, #index{} = Index, MatchSeq, UpdateSeq) -> +set_update_seq(#index{} = Index, MatchSeq, UpdateSeq) -> ReqBody = #{ match_update_seq => MatchSeq, update_seq => UpdateSeq }, - set_seq(ConnPid, Index, ReqBody). + set_seq(Index, ReqBody). -set_purge_seq(ConnPid, #index{} = Index, MatchSeq, PurgeSeq) -> +set_purge_seq(#index{} = Index, MatchSeq, PurgeSeq) -> ReqBody = #{ match_purge_seq => MatchSeq, purge_seq => PurgeSeq }, - set_seq(ConnPid, Index, ReqBody). + set_seq(Index, ReqBody). -set_seq(ConnPid, #index{} = Index, ReqBody) -> - Resp = send_direct_if_enabled( - ConnPid, index_url(Index), [?JSON_CONTENT_TYPE], post, jiffy:encode(ReqBody), [] +set_seq(#index{} = Index, ReqBody) -> + Resp = send_if_enabled( + index_path(Index), [?JSON_CONTENT_TYPE], <<"POST">>, jiffy:encode(ReqBody) ), case Resp of - {ok, "204", _, _} -> + {ok, 200, _, _} -> ok; {ok, StatusCode, _, RespBody} -> {error, jaxrs_error(StatusCode, RespBody)}; @@ -210,90 +214,37 @@ set_seq(ConnPid, #index{} = Index, ReqBody) -> send_error(Reason) end. -%% wait for enough async responses to reduce the Queue to Min length. -drain_async_responses(Queue0, Min) when Min >= 0 -> - case queue:len(Queue0) > Min of - true -> - {{value, ReqId}, Queue1} = queue:out(Queue0), - wait_for_response(ReqId), - drain_async_responses(Queue1, Min); - false -> - Queue0 - end. - -wait_for_response(ReqId) -> - case drain_async_response(ReqId) of - {ok, "204", _Headers, _Body} -> - ok; - {ok, StatusCode, _Headers, RespBody} -> - exit({error, jaxrs_error(StatusCode, RespBody)}) - end. - -drain_async_response(ReqId) -> - drain_async_response(ReqId, undefined, undefined, undefined). - -drain_async_response(ReqId, Code0, Headers0, Body0) -> - receive - {ibrowse_async_headers, ReqId, Code1, Headers1} -> - drain_async_response(ReqId, Code1, Headers1, Body0); - {ibrowse_async_response, ReqId, Body1} -> - drain_async_response(ReqId, Code0, Headers0, Body1); - {ibrowse_async_response_end, ReqId} -> - {ok, Code0, Headers0, Body0} - end. - %% private functions -index_path(Path) -> - lists:flatten( - io_lib:format( - "~s/index/~s", - [ - nouveau_util:nouveau_url(), - couch_util:url_encode(Path) - ] - ) - ). +index_path(Path) when is_binary(Path) -> + [<<"/index/">>, couch_util:url_encode(Path)]; +index_path(#index{} = Index) -> + [<<"/index/">>, couch_util:url_encode(nouveau_util:index_name(Index))]. -index_url(#index{} = Index) -> - lists:flatten( - io_lib:format( - "~s/index/~s", - [ - nouveau_util:nouveau_url(), - couch_util:url_encode(nouveau_util:index_name(Index)) - ] - ) - ). +doc_path(#index{} = Index, DocId) -> + [ + <<"/index/">>, + couch_util:url_encode(nouveau_util:index_name(Index)), + <<"/doc/">>, + couch_util:url_encode(DocId) + ]. -doc_url(#index{} = Index, DocId) -> - lists:flatten( - io_lib:format( - "~s/index/~s/doc/~s", - [ - nouveau_util:nouveau_url(), - couch_util:url_encode(nouveau_util:index_name(Index)), - couch_util:url_encode(DocId) - ] - ) - ). +search_path(#index{} = Index) -> + [index_path(Index), <<"/search">>]. -search_url(IndexName) -> - index_url(IndexName) ++ "/search". - -jaxrs_error("400", Body) -> +jaxrs_error(400, Body) -> {bad_request, message(Body)}; -jaxrs_error("404", Body) -> +jaxrs_error(404, Body) -> {not_found, message(Body)}; -jaxrs_error("405", Body) -> +jaxrs_error(405, Body) -> {method_not_allowed, message(Body)}; -jaxrs_error("409", Body) -> +jaxrs_error(409, Body) -> {conflict, message(Body)}; -jaxrs_error("417", Body) -> +jaxrs_error(417, Body) -> {expectation_failed, message(Body)}; -jaxrs_error("422", Body) -> +jaxrs_error(422, Body) -> {bad_request, lists:join(" and ", errors(Body))}; -jaxrs_error("500", Body) -> +jaxrs_error(500, Body) -> {internal_server_error, message(Body)}. send_error({conn_failed, _}) -> @@ -309,71 +260,44 @@ errors(Body) -> Json = jiffy:decode(Body, [return_maps]), maps:get(<<"errors">>, Json). -send_if_enabled(Url, Header, Method) -> - send_if_enabled(Url, Header, Method, []). +send_if_enabled(Path, ReqHeaders, Method) -> + send_if_enabled(Path, ReqHeaders, Method, <<>>). -send_if_enabled(Url, Header, Method, Body) -> - send_if_enabled(Url, Header, Method, Body, []). +send_if_enabled(Path, ReqHeaders, Method, ReqBody) -> + send_if_enabled(Path, ReqHeaders, Method, ReqBody, 5). -send_if_enabled(Url, Header, Method, Body, Options0) -> +send_if_enabled(Path, ReqHeaders, Method, ReqBody, RemainingTries) -> case nouveau:enabled() of true -> - Options1 = ibrowse_options(Options0), - retry_if_connection_closes(fun() -> - ibrowse:send_req(Url, Header, Method, Body, Options1) - end); + case + gun_pool:request( + Method, + Path, + [nouveau_gun:host_header() | ReqHeaders], + ReqBody + ) + of + {async, PoolStreamRef} -> + Timeout = config:get_integer("nouveau", "request_timeout", 30000), + case gun_pool:await(PoolStreamRef, Timeout) of + {response, fin, Status, RespHeaders} -> + {ok, Status, RespHeaders, []}; + {response, nofin, Status, RespHeaders} -> + case gun_pool:await_body(PoolStreamRef, Timeout) of + {ok, RespBody} -> + {ok, Status, RespHeaders, RespBody}; + {error, Reason} -> + {error, Reason} + end; + {error, Reason} -> + {error, Reason} + end; + {error, no_connection_available, _Reason} when RemainingTries > 0 -> + timer:sleep(1000), + send_if_enabled(Path, ReqHeaders, Method, ReqBody, RemainingTries - 1); + {error, _Type, Reason} -> + {error, Reason} + end; false -> {error, nouveau_not_enabled} end. - -send_direct_if_enabled(ConnPid, Url, Header, Method, Body, Options0) -> - case nouveau:enabled() of - true -> - Options1 = ibrowse_options(Options0), - retry_if_connection_closes(fun() -> - ibrowse:send_req_direct(ConnPid, Url, Header, Method, Body, Options1) - end); - false -> - {error, nouveau_not_enabled} - end. - -retry_if_connection_closes(Fun) -> - MaxRetries = max(1, config:get_integer("nouveau", "max_retries", 5)), - retry_if_connection_closes(Fun, MaxRetries). - -retry_if_connection_closes(_Fun, 0) -> - {error, connection_closed}; -retry_if_connection_closes(Fun, N) when is_integer(N), N > 0 -> - case Fun() of - {error, connection_closed} -> - couch_stats:increment_counter([nouveau, connection_closed_errors]), - timer:sleep(1000), - retry_if_connection_closes(Fun, N - 1); - Else -> - Else - end. - -ibrowse_options(BaseOptions) when is_list(BaseOptions) -> - CACertFile = config:get("nouveau", "ssl_cacert_file"), - KeyFile = config:get("nouveau", "ssl_key_file"), - CertFile = config:get("nouveau", "ssl_cert_file"), - Password = config:get("nouveau", "ssl_password"), - if - KeyFile /= undefined andalso CertFile /= undefined -> - CertKeyConf0 = #{ - certfile => CertFile, - keyfile => KeyFile, - password => Password, - cacertfile => CACertFile - }, - CertKeyConf1 = maps:filter(fun remove_undefined/2, CertKeyConf0), - SSLOptions = [{certs_keys, [CertKeyConf1]}], - [{ssl_options, SSLOptions} | BaseOptions]; - true -> - BaseOptions - end. - -remove_undefined(_Key, undefined) -> - false; -remove_undefined(_Key, _Value) -> - true. diff --git a/src/nouveau/src/nouveau_bookmark.erl b/src/nouveau/src/nouveau_bookmark.erl index 08aeb0f735..3b0878f726 100644 --- a/src/nouveau/src/nouveau_bookmark.erl +++ b/src/nouveau/src/nouveau_bookmark.erl @@ -51,7 +51,7 @@ unpack(DbName, PackedBookmark) when is_list(PackedBookmark) -> unpack(DbName, list_to_binary(PackedBookmark)); unpack(DbName, PackedBookmark) when is_binary(PackedBookmark) -> Bookmark = jiffy:decode(b64url:decode(PackedBookmark), [return_maps]), - maps:from_list([{range_of(DbName, V), V} || V <- Bookmark]). + #{range_of(DbName, V) => V || V <- Bookmark}. pack(nil) -> null; diff --git a/src/nouveau/src/nouveau_fabric_cleanup.erl b/src/nouveau/src/nouveau_fabric_cleanup.erl index cd4128fb1d..75c2190b8d 100644 --- a/src/nouveau/src/nouveau_fabric_cleanup.erl +++ b/src/nouveau/src/nouveau_fabric_cleanup.erl @@ -14,30 +14,52 @@ -module(nouveau_fabric_cleanup). --include_lib("couch/include/couch_db.hrl"). - --include("nouveau.hrl"). --include_lib("mem3/include/mem3.hrl"). - --export([go/1]). +-export([go/1, go_local/3]). go(DbName) -> - {ok, DesignDocs} = fabric:design_docs(DbName), - ActiveSigs = - lists:usort( - lists:flatmap( - fun(Doc) -> active_sigs(DbName, Doc) end, - [couch_doc:from_json_obj(DD) || DD <- DesignDocs] - ) - ), - Shards = mem3:shards(DbName), - lists:foreach( - fun(Shard) -> - rexi:cast(Shard#shard.node, {nouveau_rpc, cleanup, [Shard#shard.name, ActiveSigs]}) - end, - Shards - ). + case fabric_util:get_design_doc_records(DbName) of + {ok, DDocs} when is_list(DDocs) -> + Sigs = nouveau_util:get_signatures_from_ddocs(DbName, DDocs), + Shards = mem3:shards(DbName), + ByNode = maps:groups_from_list(fun mem3:node/1, fun mem3:name/1, Shards), + Fun = fun(Node, Dbs, Acc) -> + erpc:send_request(Node, ?MODULE, go_local, [DbName, Dbs, Sigs], Node, Acc) + end, + Reqs = maps:fold(Fun, erpc:reqids_new(), ByNode), + recv(DbName, Reqs, fabric_util:abs_request_timeout()); + Error -> + couch_log:error("~p : error fetching ddocs db:~p ~p", [?MODULE, DbName, Error]), + Error + end. + +% erpc endpoint for go/1 and fabric_index_cleanup:cleanup_indexes/2 +% +go_local(DbName, Dbs, Sigs) -> + try + lists:foreach( + fun(Db) -> + Sz = byte_size(DbName), + <<"shards/", Range:17/binary, "/", DbName:Sz/binary, ".", _/binary>> = Db, + Checkpoints = nouveau_util:get_purge_checkpoints(Db), + ok = couch_index_util:cleanup_purges(Db, Sigs, Checkpoints), + Path = <<"shards/", Range/binary, "/", DbName/binary, ".*/*">>, + nouveau_api:delete_path(nouveau_util:index_name(Path), maps:keys(Sigs)) + end, + Dbs + ) + catch + error:database_does_not_exist -> + ok + end. -active_sigs(DbName, #doc{} = Doc) -> - Indexes = nouveau_util:design_doc_to_indexes(DbName, Doc), - lists:map(fun(Index) -> Index#index.sig end, Indexes). +recv(DbName, Reqs, Timeout) -> + case erpc:receive_response(Reqs, Timeout, true) of + {ok, _Label, Reqs1} -> + recv(DbName, Reqs1, Timeout); + {Error, Label, Reqs1} -> + ErrMsg = "~p : error cleaning nouveau indexes db:~p node: ~p error:~p", + couch_log:error(ErrMsg, [?MODULE, DbName, Label, Error]), + recv(DbName, Reqs1, Timeout); + no_request -> + ok + end. diff --git a/src/nouveau/src/nouveau_gun.erl b/src/nouveau/src/nouveau_gun.erl new file mode 100644 index 0000000000..75792465b4 --- /dev/null +++ b/src/nouveau/src/nouveau_gun.erl @@ -0,0 +1,162 @@ +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. + +%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- + +%% index manager ensures only one process is updating a nouveau index at a time. +%% calling update_index will block until at least one attempt has been made to +%% make the index as current as the database at the time update_index was called. + +-module(nouveau_gun). +-behaviour(gen_server). +-behaviour(config_listener). + +-export([start_link/0]). +-export([host_header/0]). + +%%% gen_server callbacks +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([handle_continue/2]). + +% config_listener callbacks +-export([handle_config_change/5]). +-export([handle_config_terminate/3]). + +-define(NOUVEAU_HOST_HEADER, nouveau_host_header). + +-record(state, { + enabled, + url +}). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +host_header() -> + persistent_term:get(?NOUVEAU_HOST_HEADER). + +init(_) -> + ok = config:listen_for_changes(?MODULE, nil), + State = #state{enabled = false, url = nouveau_util:nouveau_url()}, + {ok, State, {continue, reconfigure}}. + +handle_call(_Msg, _From, State) -> + {reply, unexpected_msg, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(restart_config_listener, State) -> + ok = config:listen_for_changes(?MODULE, nil), + {noreply, State}; +handle_info(reconfigure, State) -> + reconfigure(new_state(), State); +handle_info(Msg, State) -> + couch_log:warning("~p received unexpected message: ~p", [?MODULE, Msg]), + {noreply, State}. + +handle_continue(reconfigure, State) -> + reconfigure(new_state(), State). + +handle_config_change("nouveau", "enable", _Value, _Persist, nil) -> + whereis(?MODULE) ! reconfigure, + {ok, nil}; +handle_config_change("nouveau", "url", _Value, _Persist, nil) -> + whereis(?MODULE) ! reconfigure, + {ok, nil}; +handle_config_change(_Section, _Key, _Value, _Persist, nil) -> + {ok, nil}. + +handle_config_terminate(_Server, stop, nil) -> + ok; +handle_config_terminate(_Server, _Reason, nil) -> + erlang:send_after( + 500, + whereis(?MODULE), + restart_config_listener + ). + +%% private functions + +new_state() -> + #state{enabled = nouveau:enabled(), url = nouveau_util:nouveau_url()}. + +reconfigure(#state{} = State, #state{} = State) -> + %% no change + {noreply, State}; +reconfigure(#state{enabled = false} = NewState, #state{enabled = true} = CurrState) -> + %% turning off + stop_gun(CurrState#state.url), + {noreply, NewState}; +reconfigure(#state{enabled = true} = NewState, #state{enabled = false}) -> + %% turning on + case start_gun(NewState#state.url) of + {ok, _PoolPid} -> + {noreply, NewState}; + {error, Reason} -> + {stop, Reason} + end; +reconfigure(#state{enabled = true} = NewState, #state{enabled = true} = CurrState) when + NewState#state.url /= CurrState#state.url +-> + %% changing url while on + stop_gun(CurrState#state.url), + reconfigure(NewState, CurrState#state{enabled = false}); +reconfigure(#state{enabled = false} = NewState, #state{enabled = false} = CurrState) when + NewState#state.url /= CurrState#state.url +-> + %% changing url while off + {noreply, NewState}. + +start_gun(URL) -> + #{host := Host, port := Port, scheme := Scheme} = uri_string:parse(URL), + persistent_term:put(?NOUVEAU_HOST_HEADER, {<<"host">>, [Host, $:, integer_to_binary(Port)]}), + PoolSize = config:get_integer("nouveau", "pool_size", 10), + CACertFile = config:get("nouveau", "ssl_cacert_file"), + KeyFile = config:get("nouveau", "ssl_key_file"), + CertFile = config:get("nouveau", "ssl_cert_file"), + Password = config:get("nouveau", "ssl_password"), + Transport = scheme_to_transport(Scheme), + BaseConnOptions = #{transport => Transport, protocols => [http2]}, + ConnOptions = + if + Transport == tls andalso KeyFile /= undefined andalso CertFile /= undefined -> + CertKeyConf0 = #{ + certfile => CertFile, + keyfile => KeyFile, + password => Password, + cacertfile => CACertFile + }, + CertKeyConf1 = maps:filter(fun remove_undefined/2, CertKeyConf0), + BaseConnOptions#{ + tls_opts => [{certs_keys, [CertKeyConf1]}] + }; + true -> + BaseConnOptions + end, + gun_pool:start_pool(Host, Port, #{size => PoolSize, conn_opts => ConnOptions}). + +stop_gun(URL) -> + #{host := Host, port := Port, scheme := Scheme} = uri_string:parse(URL), + gun_pool:stop_pool(Host, Port, #{transport => scheme_to_transport(Scheme)}). + +remove_undefined(_Key, Value) -> + Value /= undefined. + +scheme_to_transport("http") -> + tcp; +scheme_to_transport("https") -> + tls. diff --git a/src/nouveau/src/nouveau_index_manager.erl b/src/nouveau/src/nouveau_index_manager.erl index eb18bc6f7f..45f7a1e8d7 100644 --- a/src/nouveau/src/nouveau_index_manager.erl +++ b/src/nouveau/src/nouveau_index_manager.erl @@ -36,9 +36,6 @@ handle_info/2 ]). -% config_listener api --export([handle_config_change/5, handle_config_terminate/3]). - -export([handle_db_event/3]). -define(BY_DBSIG, nouveau_by_dbsig). @@ -60,8 +57,6 @@ init(_) -> ets:new(?BY_DBSIG, [set, named_table]), ets:new(?BY_REF, [set, named_table]), couch_event:link_listener(?MODULE, handle_db_event, nil, [all_dbs]), - configure_ibrowse(nouveau_util:nouveau_url()), - ok = config:listen_for_changes(?MODULE, nil), {ok, nil}. handle_call({update, #index{} = Index0}, From, State) -> @@ -131,31 +126,3 @@ handle_db_event(DbName, deleted, State) -> {ok, State}; handle_db_event(_DbName, _Event, State) -> {ok, State}. - -handle_config_change("nouveau", "url", URL, _Persist, State) -> - configure_ibrowse(URL), - {ok, State}; -handle_config_change(_Section, _Key, _Value, _Persist, State) -> - {ok, State}. - -handle_config_terminate(_Server, stop, _State) -> - ok; -handle_config_terminate(_Server, _Reason, _State) -> - erlang:send_after( - 5000, - whereis(?MODULE), - restart_config_listener - ). - -configure_ibrowse(URL) -> - #{host := Host, port := Port} = uri_string:parse(URL), - ibrowse:set_max_sessions( - Host, - Port, - nouveau_util:max_sessions() - ), - ibrowse:set_max_pipeline_size( - Host, - Port, - nouveau_util:max_pipeline_size() - ). diff --git a/src/nouveau/src/nouveau_index_updater.erl b/src/nouveau/src/nouveau_index_updater.erl index efed245db4..3952a893f2 100644 --- a/src/nouveau/src/nouveau_index_updater.erl +++ b/src/nouveau/src/nouveau_index_updater.erl @@ -33,10 +33,7 @@ changes_done, total_changes, exclude_idrevs, - reqids, - conn_pid, - update_seq, - max_pipeline_size + update_seq }). -record(purge_acc, { @@ -79,12 +76,11 @@ update(#index{} = Index) -> %% update status every half second couch_task_status:set_update_frequency(500), - {ok, ConnPid} = ibrowse:spawn_link_worker_process(nouveau_util:nouveau_url()), PurgeAcc0 = #purge_acc{ index_update_seq = IndexUpdateSeq, index_purge_seq = IndexPurgeSeq }, - {ok, PurgeAcc1} = purge_index(ConnPid, Db, Index, PurgeAcc0), + {ok, PurgeAcc1} = purge_index(Db, Index, PurgeAcc0), NewCurSeq = couch_db:get_update_seq(Db), Proc = get_os_process(Index#index.def_lang), @@ -98,18 +94,13 @@ update(#index{} = Index) -> changes_done = 0, total_changes = TotalChanges, exclude_idrevs = PurgeAcc1#purge_acc.exclude_list, - reqids = queue:new(), - conn_pid = ConnPid, - update_seq = PurgeAcc1#purge_acc.index_update_seq, - max_pipeline_size = nouveau_util:max_pipeline_size() + update_seq = PurgeAcc1#purge_acc.index_update_seq }, {ok, Acc1} = couch_db:fold_changes( Db, Acc0#acc.update_seq, fun load_docs/2, Acc0, [] ), - nouveau_api:drain_async_responses(Acc1#acc.reqids, 0), - exit(nouveau_api:set_update_seq(ConnPid, Index, Acc1#acc.update_seq, NewCurSeq)) + exit(nouveau_api:set_update_seq(Index, Acc1#acc.update_seq, NewCurSeq)) after - ibrowse:stop_worker_process(ConnPid), ret_os_process(Proc) end end @@ -119,11 +110,7 @@ update(#index{} = Index) -> load_docs(#full_doc_info{id = <<"_design/", _/binary>>}, #acc{} = Acc) -> {ok, Acc}; -load_docs(FDI, #acc{} = Acc0) -> - %% block for responses so we stay under the max pipeline size - ReqIds1 = nouveau_api:drain_async_responses(Acc0#acc.reqids, Acc0#acc.max_pipeline_size), - Acc1 = Acc0#acc{reqids = ReqIds1}, - +load_docs(FDI, #acc{} = Acc1) -> couch_task_status:update([ {changes_done, Acc1#acc.changes_done}, {progress, (Acc1#acc.changes_done * 100) div Acc1#acc.total_changes} @@ -138,7 +125,6 @@ load_docs(FDI, #acc{} = Acc0) -> false -> case update_or_delete_index( - Acc1#acc.conn_pid, Acc1#acc.db, Acc1#acc.index, Acc1#acc.update_seq, @@ -146,10 +132,9 @@ load_docs(FDI, #acc{} = Acc0) -> Acc1#acc.proc ) of - {ibrowse_req_id, ReqId} -> + ok -> Acc1#acc{ - update_seq = DI#doc_info.high_seq, - reqids = queue:in(ReqId, Acc1#acc.reqids) + update_seq = DI#doc_info.high_seq }; {error, Reason} -> exit({error, Reason}) @@ -157,11 +142,11 @@ load_docs(FDI, #acc{} = Acc0) -> end, {ok, Acc2#acc{changes_done = Acc2#acc.changes_done + 1}}. -update_or_delete_index(ConnPid, Db, #index{} = Index, MatchSeq, #doc_info{} = DI, Proc) -> +update_or_delete_index(Db, #index{} = Index, MatchSeq, #doc_info{} = DI, Proc) -> #doc_info{id = Id, high_seq = Seq, revs = [#rev_info{deleted = Del} | _]} = DI, case Del of true -> - nouveau_api:delete_doc_async(ConnPid, Index, Id, MatchSeq, Seq); + nouveau_api:delete_doc(Index, Id, MatchSeq, Seq); false -> {ok, Doc} = couch_db:open_doc(Db, DI, []), Json = couch_doc:to_json_obj(Doc, []), @@ -175,10 +160,10 @@ update_or_delete_index(ConnPid, Db, #index{} = Index, MatchSeq, #doc_info{} = DI end, case Fields of [] -> - nouveau_api:delete_doc_async(ConnPid, Index, Id, MatchSeq, Seq); + nouveau_api:delete_doc(Index, Id, MatchSeq, Seq); _ -> - nouveau_api:update_doc_async( - ConnPid, Index, Id, MatchSeq, Seq, Partition, Fields + nouveau_api:update_doc( + Index, Id, MatchSeq, Seq, Partition, Fields ) end end. @@ -223,7 +208,7 @@ index_definition(#index{} = Index) -> <<"field_analyzers">> => Index#index.field_analyzers }. -purge_index(ConnPid, Db, Index, #purge_acc{} = PurgeAcc0) -> +purge_index(Db, Index, #purge_acc{} = PurgeAcc0) -> Proc = get_os_process(Index#index.def_lang), try true = proc_prompt(Proc, [<<"add_fun">>, Index#index.def, <<"nouveau">>]), @@ -232,7 +217,7 @@ purge_index(ConnPid, Db, Index, #purge_acc{} = PurgeAcc0) -> case couch_db:get_full_doc_info(Db, Id) of not_found -> ok = nouveau_api:purge_doc( - ConnPid, Index, Id, PurgeAcc1#purge_acc.index_purge_seq, PurgeSeq + Index, Id, PurgeAcc1#purge_acc.index_purge_seq, PurgeSeq ), PurgeAcc1#purge_acc{index_purge_seq = PurgeSeq}; FDI -> @@ -243,7 +228,6 @@ purge_index(ConnPid, Db, Index, #purge_acc{} = PurgeAcc0) -> PurgeAcc1; false -> update_or_delete_index( - ConnPid, Db, Index, PurgeAcc1#purge_acc.index_update_seq, @@ -265,7 +249,7 @@ purge_index(ConnPid, Db, Index, #purge_acc{} = PurgeAcc0) -> ), DbPurgeSeq = couch_db:get_purge_seq(Db), ok = nouveau_api:set_purge_seq( - ConnPid, Index, PurgeAcc3#purge_acc.index_purge_seq, DbPurgeSeq + Index, PurgeAcc3#purge_acc.index_purge_seq, DbPurgeSeq ), update_local_doc(Db, Index, DbPurgeSeq), {ok, PurgeAcc3} diff --git a/src/nouveau/src/nouveau_rpc.erl b/src/nouveau/src/nouveau_rpc.erl index 5d954b5f3e..2037c7e7ef 100644 --- a/src/nouveau/src/nouveau_rpc.erl +++ b/src/nouveau/src/nouveau_rpc.erl @@ -17,52 +17,62 @@ -export([ search/3, - info/2, - cleanup/2 + info/2 ]). -include("nouveau.hrl"). -import(nouveau_util, [index_path/1]). -search(DbName, #index{} = Index0, QueryArgs0) -> +search(DbName, #index{} = Index, #{} = QueryArgs) -> + search(DbName, #index{} = Index, QueryArgs, 0). + +search(DbName, #index{} = Index0, QueryArgs0, UpdateLatency) -> %% Incorporate the shard name into the record. Index1 = Index0#index{dbname = DbName}, - %% get minimum seqs for search - {MinUpdateSeq, MinPurgeSeq} = nouveau_index_updater:get_db_info(Index1), + Update = maps:get(update, QueryArgs0, true), %% Incorporate min seqs into the query args. - QueryArgs1 = QueryArgs0#{ - min_update_seq => MinUpdateSeq, - min_purge_seq => MinPurgeSeq - }, - Update = maps:get(update, QueryArgs1, true), - - %% check if index is up to date - T0 = erlang:monotonic_time(), - case Update andalso nouveau_index_updater:outdated(Index1) of - true -> - case nouveau_index_manager:update_index(Index1) of - ok -> - ok; - {error, Reason} -> - rexi:reply({error, Reason}) - end; - false -> - ok; - {error, Reason} -> - rexi:reply({error, Reason}) - end, - T1 = erlang:monotonic_time(), - UpdateLatency = erlang:convert_time_unit(T1 - T0, native, millisecond), + QueryArgs1 = + case Update of + true -> + %% get minimum seqs for search + {MinUpdateSeq, MinPurgeSeq} = nouveau_index_updater:get_db_info(Index1), + QueryArgs0#{ + min_update_seq => MinUpdateSeq, + min_purge_seq => MinPurgeSeq + }; + false -> + QueryArgs0#{ + min_update_seq => 0, + min_purge_seq => 0 + } + end, %% Run the search case nouveau_api:search(Index1, QueryArgs1) of {ok, Response} -> rexi:reply({ok, Response#{update_latency => UpdateLatency}}); - {error, stale_index} -> - %% try again. - search(DbName, Index0, QueryArgs0); + {error, stale_index} when Update -> + update_and_retry(DbName, Index0, QueryArgs0, UpdateLatency); + {error, {not_found, _}} when Update -> + update_and_retry(DbName, Index0, QueryArgs0, UpdateLatency); + Else -> + rexi:reply(Else) + end. + +update_and_retry(DbName, Index, QueryArgs, UpdateLatency) -> + T0 = erlang:monotonic_time(), + case nouveau_index_manager:update_index(Index#index{dbname = DbName}) of + ok -> + T1 = erlang:monotonic_time(), + search( + DbName, + Index, + QueryArgs, + UpdateLatency + + erlang:convert_time_unit(T1 - T0, native, millisecond) + ); Else -> rexi:reply(Else) end. @@ -77,7 +87,3 @@ info(DbName, #index{} = Index0) -> {error, Reason} -> rexi:reply({error, Reason}) end. - -cleanup(DbName, Exclusions) -> - nouveau_api:delete_path(nouveau_util:index_name(DbName), Exclusions), - rexi:reply(ok). diff --git a/src/nouveau/src/nouveau_sup.erl b/src/nouveau/src/nouveau_sup.erl index 3547b43fa1..65afe744a3 100644 --- a/src/nouveau/src/nouveau_sup.erl +++ b/src/nouveau/src/nouveau_sup.erl @@ -23,6 +23,7 @@ start_link() -> init(_Args) -> Children = [ + child(nouveau_gun), child(nouveau_index_manager) ], {ok, {{one_for_one, 10, 1}, couch_epi:register_service(nouveau_epi, Children)}}. diff --git a/src/nouveau/src/nouveau_util.erl b/src/nouveau/src/nouveau_util.erl index b6dd0fcbd5..3df43f2ffc 100644 --- a/src/nouveau/src/nouveau_util.erl +++ b/src/nouveau/src/nouveau_util.erl @@ -27,9 +27,9 @@ maybe_create_local_purge_doc/2, get_local_purge_doc_id/1, get_local_purge_doc_body/3, - nouveau_url/0, - max_sessions/0, - max_pipeline_size/0 + get_purge_checkpoints/1, + get_signatures_from_ddocs/2, + nouveau_url/0 ]). index_name(Path) when is_binary(Path) -> @@ -75,16 +75,15 @@ design_doc_to_index(DbName, #doc{id = Id, body = {Fields}}, IndexName) -> undefined -> {error, InvalidDDocError}; Def -> - Sig = ?l2b( - couch_util:to_hex( + Sig = + couch_util:to_hex_bin( crypto:hash( sha256, ?term_to_bin( {DefaultAnalyzer, FieldAnalyzers, Def} ) ) - ) - ), + ), {ok, #index{ dbname = DbName, default_analyzer = DefaultAnalyzer, @@ -177,6 +176,9 @@ maybe_create_local_purge_doc(Db, Index) -> get_local_purge_doc_id(Sig) -> iolist_to_binary([?LOCAL_DOC_PREFIX, "purge-", "nouveau-", Sig]). +get_purge_checkpoints(Db) -> + couch_index_util:get_purge_checkpoints(Db, <<"nouveau">>). + get_local_purge_doc_body(LocalDocId, PurgeSeq, Index) -> #index{ name = IdxName, @@ -196,11 +198,13 @@ get_local_purge_doc_body(LocalDocId, PurgeSeq, Index) -> ]}, couch_doc:from_json_obj(JsonList). -nouveau_url() -> - config:get("nouveau", "url", "http://127.0.0.1:5987"). +get_signatures_from_ddocs(DbName, DesignDocs) -> + SigList = lists:flatmap(fun(Doc) -> active_sigs(DbName, Doc) end, DesignDocs), + #{Sig => true || Sig <- SigList}. -max_sessions() -> - config:get_integer("nouveau", "max_sessions", 100). +active_sigs(DbName, #doc{} = Doc) -> + Indexes = nouveau_util:design_doc_to_indexes(DbName, Doc), + lists:map(fun(Index) -> Index#index.sig end, Indexes). -max_pipeline_size() -> - config:get_integer("nouveau", "max_pipeline_size", 1000). +nouveau_url() -> + config:get("nouveau", "url", "http://127.0.0.1:5987"). diff --git a/src/rexi/src/rexi.erl b/src/rexi/src/rexi.erl index 02d3a9e555..99333c02ff 100644 --- a/src/rexi/src/rexi.erl +++ b/src/rexi/src/rexi.erl @@ -275,7 +275,7 @@ wait_for_ack(Count, Timeout) -> end. drain_acks(Count) when Count < 0 -> - erlang:error(mismatched_rexi_ack); + error(mismatched_rexi_ack); drain_acks(Count) -> receive {rexi_ack, N} -> drain_acks(Count - N) diff --git a/src/rexi/src/rexi_monitor.erl b/src/rexi/src/rexi_monitor.erl index 7fe66db71d..8208b7691b 100644 --- a/src/rexi/src/rexi_monitor.erl +++ b/src/rexi/src/rexi_monitor.erl @@ -27,7 +27,7 @@ start(Procs) -> ), spawn_link(fun() -> [notify_parent(Parent, P, noconnect) || P <- Skip], - [erlang:monitor(process, P) || P <- Mon], + [monitor(process, P) || P <- Mon], wait_monitors(Parent) end). diff --git a/src/rexi/src/rexi_server.erl b/src/rexi/src/rexi_server.erl index b2df65c719..9028616bd4 100644 --- a/src/rexi/src/rexi_server.erl +++ b/src/rexi/src/rexi_server.erl @@ -206,7 +206,7 @@ notify_caller({Caller, Ref}, Reason) -> kill_worker(FromRef, #st{clients = Clients} = St) -> case find_worker(FromRef, Clients) of #job{worker = KeyRef, worker_pid = Pid} = Job -> - erlang:demonitor(KeyRef), + demonitor(KeyRef), exit(Pid, kill), remove_job(Job, St), ok; diff --git a/src/rexi/src/rexi_server_mon.erl b/src/rexi/src/rexi_server_mon.erl index 1677bd3108..7049cfa216 100644 --- a/src/rexi/src/rexi_server_mon.erl +++ b/src/rexi/src/rexi_server_mon.erl @@ -57,7 +57,6 @@ aggregate_queue_len(ChildMod) -> % Mem3 cluster callbacks cluster_unstable(Server) -> - couch_log:notice("~s : cluster unstable", [?MODULE]), gen_server:cast(Server, cluster_unstable), Server. @@ -75,7 +74,7 @@ init(ChildMod) -> ?CLUSTER_STABILITY_PERIOD_SEC ), start_servers(ChildMod), - couch_log:notice("~s : started servers", [ChildMod]), + couch_log:info("~s : started servers", [ChildMod]), {ok, ChildMod}. handle_call(status, _From, ChildMod) -> @@ -93,13 +92,13 @@ handle_call(Msg, _From, St) -> % can be started, but do not immediately stop nodes, defer that till cluster % stabilized. handle_cast(cluster_unstable, ChildMod) -> - couch_log:notice("~s : cluster unstable", [ChildMod]), + couch_log:info("~s : cluster unstable", [ChildMod]), start_servers(ChildMod), {noreply, ChildMod}; % When cluster is stable, start any servers for new nodes and stop servers for % the ones that disconnected. handle_cast(cluster_stable, ChildMod) -> - couch_log:notice("~s : cluster stable", [ChildMod]), + couch_log:info("~s : cluster stable", [ChildMod]), start_servers(ChildMod), stop_servers(ChildMod), {noreply, ChildMod}; @@ -153,7 +152,7 @@ start_server(ChildMod, ChildId) -> {ok, Pid} -> {ok, Pid}; Else -> - erlang:error(Else) + error(Else) end. stop_server(ChildMod, ChildId) -> diff --git a/src/rexi/src/rexi_utils.erl b/src/rexi/src/rexi_utils.erl index 146d0238ac..598ef8e7e0 100644 --- a/src/rexi/src/rexi_utils.erl +++ b/src/rexi/src/rexi_utils.erl @@ -38,7 +38,7 @@ send(Dest, Msg) -> recv(Refs, Keypos, Fun, Acc0, infinity, PerMsgTO) -> process_mailbox(Refs, Keypos, Fun, Acc0, nil, PerMsgTO); recv(Refs, Keypos, Fun, Acc0, GlobalTimeout, PerMsgTO) -> - TimeoutRef = erlang:make_ref(), + TimeoutRef = make_ref(), TRef = erlang:send_after(GlobalTimeout, self(), {timeout, TimeoutRef}), try process_mailbox(Refs, Keypos, Fun, Acc0, TimeoutRef, PerMsgTO) diff --git a/src/rexi/test/rexi_tests.erl b/src/rexi/test/rexi_tests.erl index 18b05b545c..cc2c0cc1a5 100644 --- a/src/rexi/test/rexi_tests.erl +++ b/src/rexi/test/rexi_tests.erl @@ -143,7 +143,7 @@ t_stream2(_) -> t_stream2_acks(_) -> Ref = rexi:cast(node(), {?MODULE, rpc_test_fun, [stream2_acks]}), {WPid, _Tag} = From = stream_init(Ref), - Mon = erlang:monitor(process, WPid), + Mon = monitor(process, WPid), rexi:stream_start(From), ?assertEqual(a, recv(Ref)), ?assertEqual(b, recv(Ref)), @@ -168,7 +168,7 @@ t_stream2_acks(_) -> t_stream2_cancel(_) -> Ref = rexi:cast(node(), {?MODULE, rpc_test_fun, [stream2_init]}), {WPid, _Tag} = From = stream_init(Ref), - Mon = erlang:monitor(process, WPid), + Mon = monitor(process, WPid), rexi:stream_cancel(From), Res = receive diff --git a/src/setup/src/setup.erl b/src/setup/src/setup.erl index 7a38e69cbe..31bc1f58ef 100644 --- a/src/setup/src/setup.erl +++ b/src/setup/src/setup.erl @@ -376,7 +376,7 @@ add_node_int(Options, true) -> end. get_port(Port) when is_integer(Port) -> - list_to_binary(integer_to_list(Port)); + integer_to_binary(Port); get_port(Port) when is_list(Port) -> list_to_binary(Port); get_port(Port) when is_binary(Port) -> diff --git a/src/setup/test/t-frontend-setup.sh b/src/setup/test/t-frontend-setup.sh index e025cfba2d..106312dece 100755 --- a/src/setup/test/t-frontend-setup.sh +++ b/src/setup/test/t-frontend-setup.sh @@ -64,8 +64,8 @@ curl a:b@127.0.0.1:25984/_node/node2@127.0.0.1/_config/cluster/n curl a:b@127.0.0.1:15984/_node/node1@127.0.0.1/_config/couchdb/uuid curl a:b@127.0.0.1:15984/_node/node2@127.0.0.1/_config/couchdb/uuid -curl a:b@127.0.0.1:15984/_node/node1@127.0.0.1/_config/couch_httpd_auth/secret -curl a:b@127.0.0.1:15984/_node/node2@127.0.0.1/_config/couch_httpd_auth/secret +curl a:b@127.0.0.1:15984/_node/node1@127.0.0.1/_config/chttpd_auth/secret +curl a:b@127.0.0.1:15984/_node/node2@127.0.0.1/_config/chttpd_auth/secret echo "YAY ALL GOOD" diff --git a/src/smoosh/src/smoosh.erl b/src/smoosh/src/smoosh.erl index 68e8d1828e..00dc186ca5 100644 --- a/src/smoosh/src/smoosh.erl +++ b/src/smoosh/src/smoosh.erl @@ -64,7 +64,14 @@ fold_local_shards(Fun, Acc0) -> enqueue_views(ShardName) -> DbName = mem3:dbname(ShardName), - {ok, DDocs} = fabric:design_docs(DbName), + DDocs = + case fabric:design_docs(DbName) of + {ok, Resp} when is_list(Resp) -> + Resp; + Else -> + couch_log:debug("Invalid design docs: ~p~n", [Else]), + [] + end, [sync_enqueue({ShardName, id(DDoc)}) || DDoc <- DDocs]. id(#doc{id = Id}) -> diff --git a/src/smoosh/src/smoosh_channel.erl b/src/smoosh/src/smoosh_channel.erl index 3cfbcdec69..eabb751adb 100644 --- a/src/smoosh/src/smoosh_channel.erl +++ b/src/smoosh/src/smoosh_channel.erl @@ -169,8 +169,8 @@ handle_info({Ref, {ok, Pid}}, #state{} = State) when is_reference(Ref) -> LogMsg = "~s: Started compaction for ~s", LogArgs = [Name, smoosh_utils:stringify(Key)], couch_log:Level(LogMsg, LogArgs), - erlang:monitor(process, Pid), - erlang:demonitor(Ref, [flush]), + monitor(process, Pid), + demonitor(Ref, [flush]), Active1 = Active#{Key => Pid}, State1 = State#state{active = Active1, starting = Starting1}, {noreply, set_status(State1)}; @@ -350,7 +350,7 @@ start_compact(#state{} = State, {Shard, GroupId} = Key) -> case couch_index_server:get_index(couch_mrview_index, Shard, GroupId) of {ok, Pid} -> schedule_cleanup_index_files(Shard), - Ref = erlang:monitor(process, Pid), + Ref = monitor(process, Pid), Pid ! {'$gen_call', {self(), Ref}, compact}, State#state{starting = Starting#{Ref => Key}}; Error -> @@ -370,12 +370,12 @@ start_compact(#state{} = State, Db) -> case couch_db:get_compactor_pid(Db) of nil -> DbPid = couch_db:get_pid(Db), - Ref = erlang:monitor(process, DbPid), + Ref = monitor(process, DbPid), DbPid ! {'$gen_call', {self(), Ref}, start_compact}, State#state{starting = Starting#{Ref => Key}}; % Compaction is already running, so monitor existing compaction pid. CPid when is_pid(CPid) -> - erlang:monitor(process, CPid), + monitor(process, CPid), Level = smoosh_utils:log_level("compaction_log_level", "notice"), LogMsg = "~s : db ~s continuing compaction", LogArgs = [Name, smoosh_utils:stringify(Key)], @@ -398,7 +398,7 @@ maybe_remonitor_cpid(#state{} = State, DbName, Reason) when is_binary(DbName) -> re_enqueue(DbName), State; CPid when is_pid(CPid) -> - erlang:monitor(process, CPid), + monitor(process, CPid), Level = smoosh_utils:log_level("compaction_log_level", "notice"), LogMsg = "~s: ~s compaction already running. Re-monitor Pid ~p", LogArgs = [Name, smoosh_utils:stringify(DbName), CPid], @@ -451,7 +451,7 @@ re_enqueue(Obj) -> cleanup_index_files(DbName) -> case should_clean_up_indices() of - true -> fabric:cleanup_index_files(DbName); + true -> fabric:cleanup_index_files_this_node(DbName); false -> ok end. diff --git a/src/smoosh/src/smoosh_persist.erl b/src/smoosh/src/smoosh_persist.erl index c1519f65fa..f615fcbb93 100644 --- a/src/smoosh/src/smoosh_persist.erl +++ b/src/smoosh/src/smoosh_persist.erl @@ -71,8 +71,8 @@ persist(true, Waiting, Active, Starting) -> % already running. We want them to be the first ones to continue after % restart. We're relying on infinity sorting higher than float and integer % numeric values here. - AMap = maps:map(fun(_, _) -> infinity end, Active), - SMap = maps:from_list([{K, infinity} || K <- maps:values(Starting)]), + AMap = #{K => infinity || K := _Pid <- Active}, + SMap = #{K => infinity || _Ref := K <- Starting}, Path = file_path(Name), write(maps:merge(WMap, maps:merge(AMap, SMap)), Path). diff --git a/src/smoosh/test/smoosh_tests.erl b/src/smoosh/test/smoosh_tests.erl index 6861db5e18..5170248753 100644 --- a/src/smoosh/test/smoosh_tests.erl +++ b/src/smoosh/test/smoosh_tests.erl @@ -155,7 +155,7 @@ t_index_cleanup_happens_by_default(DbName) -> get_channel_pid("index_cleanup") ! unpause, {ok, _} = fabric:query_view(DbName, <<"foo">>, <<"bar">>), % View cleanup should have been invoked - meck:wait(fabric, cleanup_index_files, [DbName], 4000), + meck:wait(fabric, cleanup_index_files_this_node, [DbName], 4000), wait_view_compacted(DbName, <<"foo">>). t_index_cleanup_can_be_disabled(DbName) -> @@ -183,17 +183,17 @@ t_suspend_resume(DbName) -> ok = wait_to_enqueue(DbName), CompPid = wait_db_compactor_pid(), ok = smoosh:suspend(), - ?assertEqual({status, suspended}, erlang:process_info(CompPid, status)), + ?assertEqual({status, suspended}, process_info(CompPid, status)), ?assertEqual({1, 0, 0}, sync_status("ratio_dbs")), % Suspending twice should work too ok = smoosh:suspend(), - ?assertEqual({status, suspended}, erlang:process_info(CompPid, status)), + ?assertEqual({status, suspended}, process_info(CompPid, status)), ?assertEqual({1, 0, 0}, sync_status("ratio_dbs")), ok = smoosh:resume(), - ?assertNotEqual({status, suspended}, erlang:process_info(CompPid, status)), + ?assertNotEqual({status, suspended}, process_info(CompPid, status)), % Resuming twice should work too ok = smoosh:resume(), - ?assertNotEqual({status, suspended}, erlang:process_info(CompPid, status)), + ?assertNotEqual({status, suspended}, process_info(CompPid, status)), CompPid ! continue, wait_compacted(DbName). @@ -205,7 +205,7 @@ t_check_window_can_resume(DbName) -> ok = wait_to_enqueue(DbName), CompPid = wait_db_compactor_pid(), ok = smoosh:suspend(), - ?assertEqual({status, suspended}, erlang:process_info(CompPid, status)), + ?assertEqual({status, suspended}, process_info(CompPid, status)), get_channel_pid("ratio_dbs") ! check_window, CompPid ! continue, wait_compacted(DbName). @@ -252,7 +252,7 @@ t_checkpointing_works(DbName) -> checkpoint(), % Stop smoosh and then crash the compaction ok = application:stop(smoosh), - Ref = erlang:monitor(process, CompPid), + Ref = monitor(process, CompPid), CompPid ! {raise, exit, kapow}, receive {'DOWN', Ref, _, _, kapow} -> @@ -276,7 +276,7 @@ t_ignore_checkpoint_resume_if_compacted_already(DbName) -> checkpoint(), % Stop smoosh and then let the compaction finish ok = application:stop(smoosh), - Ref = erlang:monitor(process, CompPid), + Ref = monitor(process, CompPid), CompPid ! continue, receive {'DOWN', Ref, _, _, normal} -> ok diff --git a/src/weatherreport/src/weatherreport_check_mem3_sync.erl b/src/weatherreport/src/weatherreport_check_mem3_sync.erl index cabca5d505..7193d77d70 100644 --- a/src/weatherreport/src/weatherreport_check_mem3_sync.erl +++ b/src/weatherreport/src/weatherreport_check_mem3_sync.erl @@ -43,7 +43,7 @@ valid() -> -spec check(list()) -> [{atom(), term()}]. check(_Opts) -> - case erlang:whereis(mem3_sync) of + case whereis(mem3_sync) of undefined -> [{warning, mem3_sync_not_found}]; Pid -> diff --git a/src/weatherreport/src/weatherreport_check_node_stats.erl b/src/weatherreport/src/weatherreport_check_node_stats.erl index 6c3353dc6c..d8aa5da9f1 100644 --- a/src/weatherreport/src/weatherreport_check_node_stats.erl +++ b/src/weatherreport/src/weatherreport_check_node_stats.erl @@ -60,7 +60,7 @@ mean_to_message({Statistic, Mean}) -> -spec check(list()) -> [{atom(), term()}]. check(_Opts) -> SumOfStats = recon:node_stats(?SAMPLES, 100, fun sum_absolute_stats/2, []), - MeanStats = [{K, erlang:round(V / ?SAMPLES)} || {K, V} <- SumOfStats], + MeanStats = [{K, round(V / ?SAMPLES)} || {K, V} <- SumOfStats], lists:map(fun mean_to_message/1, MeanStats). -spec format(term()) -> {io:format(), [term()]}. diff --git a/src/weatherreport/src/weatherreport_check_nodes_connected.erl b/src/weatherreport/src/weatherreport_check_nodes_connected.erl index 3890542091..6ad369c23d 100644 --- a/src/weatherreport/src/weatherreport_check_nodes_connected.erl +++ b/src/weatherreport/src/weatherreport_check_nodes_connected.erl @@ -50,7 +50,7 @@ valid() -> -spec check(list()) -> [{atom(), term()}]. check(_Opts) -> NodeName = node(), - ConnectedNodes = [NodeName | erlang:nodes()], + ConnectedNodes = [NodeName | nodes()], Members = mem3:nodes(), [ {warning, {node_disconnected, N}} diff --git a/src/weatherreport/src/weatherreport_check_process_calls.erl b/src/weatherreport/src/weatherreport_check_process_calls.erl index b6a228aeb4..64a5585a42 100644 --- a/src/weatherreport/src/weatherreport_check_process_calls.erl +++ b/src/weatherreport/src/weatherreport_check_process_calls.erl @@ -62,7 +62,7 @@ fold_processes([{Count, {M, F, A}} | T], Acc, Lim, CallType, Opts) -> case proplists:get_value(expert, Opts) of true -> PidFun = list_to_atom("find_by_" ++ CallType ++ "_call"), - Pids = erlang:apply(recon, PidFun, [M, F]), + Pids = apply(recon, PidFun, [M, F]), Pinfos = lists:map( fun(Pid) -> Pinfo = recon:info(Pid), diff --git a/src/weatherreport/src/weatherreport_getopt.erl b/src/weatherreport/src/weatherreport_getopt.erl index 7361126300..c66f8e085f 100644 --- a/src/weatherreport/src/weatherreport_getopt.erl +++ b/src/weatherreport/src/weatherreport_getopt.erl @@ -423,7 +423,7 @@ to_type(boolean, Arg) -> true -> false; false -> - erlang:error(badarg) + error(badarg) end end; to_type(_Type, Arg) -> diff --git a/src/weatherreport/src/weatherreport_node.erl b/src/weatherreport/src/weatherreport_node.erl index d108d0f7f9..c1fad83410 100644 --- a/src/weatherreport/src/weatherreport_node.erl +++ b/src/weatherreport/src/weatherreport_node.erl @@ -75,7 +75,7 @@ local_command(Module, Function, Args, Timeout) -> "Local function call: ~p:~p(~p)", [Module, Function, Args] ), - erlang:apply(Module, Function, Args); + apply(Module, Function, Args); _ -> weatherreport_log:log( node(), diff --git a/src/weatherreport/src/weatherreport_util.erl b/src/weatherreport/src/weatherreport_util.erl index ef42505e91..c4f9fb42a6 100644 --- a/src/weatherreport/src/weatherreport_util.erl +++ b/src/weatherreport/src/weatherreport_util.erl @@ -54,7 +54,7 @@ run_command(Command) -> "Running shell command: ~s", [Command] ), - Port = erlang:open_port({spawn, Command}, [exit_status, stderr_to_stdout]), + Port = open_port({spawn, Command}, [exit_status, stderr_to_stdout]), do_read(Port, []). do_read(Port, Acc) -> diff --git a/test/elixir/lib/asserts.ex b/test/elixir/lib/asserts.ex new file mode 100644 index 0000000000..cfbd64738f --- /dev/null +++ b/test/elixir/lib/asserts.ex @@ -0,0 +1,20 @@ +defmodule Couch.Test.Asserts do + @moduledoc """ + Custom asserts. + """ + defmacro assert_on_status(resp, expected, failure_message) do + expected_list = List.wrap(expected) + + expected_msg = case expected_list do + [single] -> "Expected #{single}" + multiple -> "Expected one of #{inspect(multiple)}" + end + + quote do + status_code = unquote(resp).status_code + body = unquote(resp).body + message = "#{unquote(failure_message)} #{unquote(expected_msg)}, got: #{status_code}, body: #{inspect(body)}" + ExUnit.Assertions.assert(status_code in unquote(expected_list), "#{message}") + end + end +end diff --git a/test/elixir/test/changes_async_test.exs b/test/elixir/test/changes_async_test.exs index 4850393c72..ae8fb41b26 100644 --- a/test/elixir/test/changes_async_test.exs +++ b/test/elixir/test/changes_async_test.exs @@ -394,7 +394,7 @@ defmodule ChangesAsyncTest do end end - defp process_response(id, chunk_parser, timeout \\ 1000) do + defp process_response(id, chunk_parser, timeout \\ 3000) do receive do %HTTPotion.AsyncChunk{id: ^id} = msg -> chunk_parser.(msg) diff --git a/test/elixir/test/config/nouveau.elixir b/test/elixir/test/config/nouveau.elixir index 02157e2ca3..1c962aafac 100644 --- a/test/elixir/test/config/nouveau.elixir +++ b/test/elixir/test/config/nouveau.elixir @@ -30,6 +30,7 @@ "purge with conflicts", "index same field with different field types", "index not found", - "meta" + "meta", + "stale search" ] } diff --git a/test/elixir/test/nouveau_test.exs b/test/elixir/test/nouveau_test.exs index 5f97c0aa3c..36426d9995 100644 --- a/test/elixir/test/nouveau_test.exs +++ b/test/elixir/test/nouveau_test.exs @@ -694,6 +694,23 @@ defmodule NouveauTest do assert resp.body["update_latency"] > 0 end + @tag :with_db + test "stale search", context do + db_name = context[:db_name] + url = "/#{db_name}/_design/foo/_nouveau/bar" + create_ddoc(db_name) + + resp = Couch.get(url, query: %{q: "*:*", update: false, include_docs: true}) + assert_status_code(resp, 404) + + create_search_docs(db_name) + resp = Couch.get(url, query: %{q: "*:*", include_docs: true}) + assert_status_code(resp, 200) + ids = get_ids(resp) + # nouveau sorts by _id as tie-breaker + assert ids == ["doc1", "doc2", "doc3", "doc4"] + end + def seq(str) do String.to_integer(hd(Regex.run(~r/^[0-9]+/, str))) end diff --git a/test/elixir/test/partition_search_test.exs b/test/elixir/test/partition_search_test.exs index 1219954492..9310e701d2 100644 --- a/test/elixir/test/partition_search_test.exs +++ b/test/elixir/test/partition_search_test.exs @@ -1,5 +1,6 @@ defmodule PartitionSearchTest do use CouchTestCase + import Couch.Test.Asserts @moduletag :search @@ -22,7 +23,7 @@ defmodule PartitionSearchTest do end resp = Couch.post("/#{db_name}/_bulk_docs", headers: ["Content-Type": "application/json"], body: %{:docs => docs}, query: %{w: 3}) - assert resp.status_code in [201, 202] + assert_on_status(resp, [201, 202], "Cannot create search docs.") end def create_ddoc(db_name, opts \\ %{}) do @@ -39,7 +40,7 @@ defmodule PartitionSearchTest do ddoc = Enum.into(opts, default_ddoc) resp = Couch.put("/#{db_name}/_design/library", body: ddoc) - assert resp.status_code in [201, 202] + assert_on_status(resp, [201, 202], "Cannot create design doc.") assert Map.has_key?(resp.body, "ok") == true end @@ -56,13 +57,13 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_partition/foo/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field"}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do partitioned search.") ids = get_ids(resp) assert ids == ["foo:10", "foo:2", "foo:4", "foo:6", "foo:8"] url = "/#{db_name}/_partition/bar/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field"}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do partitioned search.") ids = get_ids(resp) assert ids == ["bar:1", "bar:3", "bar:5", "bar:7", "bar:9"] end @@ -75,7 +76,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_partition/foo/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field"}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do partitioned search.") ids = get_ids(resp) assert ids == ["foo:10", "foo:2", "foo:4", "foo:6", "foo:8"] end @@ -88,24 +89,24 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_partition/foo/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field", limit: 3}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do partitioned search.") ids = get_ids(resp) assert ids == ["foo:10", "foo:2", "foo:4"] %{:body => %{"bookmark" => bookmark}} = resp resp = Couch.get(url, query: %{q: "some:field", limit: 3, bookmark: bookmark}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do partitioned search with a bookmark.") ids = get_ids(resp) assert ids == ["foo:6", "foo:8"] resp = Couch.get(url, query: %{q: "some:field", limit: 2000, bookmark: bookmark}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do partition search with an upper bound on the limit.") ids = get_ids(resp) assert ids == ["foo:6", "foo:8"] resp = Couch.get(url, query: %{q: "some:field", limit: 2001, bookmark: bookmark}) - assert resp.status_code == 400 + assert_on_status(resp, 400, "Should fail to do partition search with over limit.") end @tag :with_db @@ -116,7 +117,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_design/library/_search/books" resp = Couch.post(url, body: %{:q => "some:field", :limit => 1}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do POST for non-partitioned db with limit.") end @tag :with_partitioned_db @@ -127,7 +128,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_partition/foo/_design/library/_search/books" resp = Couch.post(url, body: %{:q => "some:field", :limit => 1}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do POST for partitioned db with limit.") end @tag :with_partitioned_db @@ -138,7 +139,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field"}) - assert resp.status_code == 400 + assert_on_status(resp, 400, "Expected a failure to do a global query on partitioned view.") %{:body => %{"reason" => reason}} = resp assert Regex.match?(~r/mandatory for queries to this index./, reason) end @@ -151,7 +152,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_partition/foo/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field"}) - assert resp.status_code == 400 + assert_on_status(resp, 400, "Expected a failure to do a query with a global search ddoc.") %{:body => %{"reason" => reason}} = resp assert reason == "`partition` not supported on this index" end @@ -164,7 +165,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field"}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Failed to search on non-partitioned dbs.") ids = get_ids(resp) assert Enum.sort(ids) == Enum.sort(["bar:1", "bar:5", "bar:9", "foo:2", "bar:3", "foo:4", "foo:6", "bar:7", "foo:8", "foo:10"]) end @@ -177,7 +178,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field"}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Failed to search on non-partitioned dbs without the limit.") ids = get_ids(resp) assert Enum.sort(ids) == Enum.sort(["bar:1", "bar:5", "bar:9", "foo:2", "bar:3", "foo:4", "foo:6", "bar:7", "foo:8", "foo:10"]) end @@ -189,10 +190,16 @@ defmodule PartitionSearchTest do create_ddoc(db_name) url = "/#{db_name}/_design/library/_search/books" + + # score order varies by Lucene version, so captured this order first. + resp = Couch.get(url, query: %{q: "some:field"}) + assert_on_status(resp, 200, "Failed to search on non-partitioned dbs without the limit.") + expected_ids = get_ids(resp) + + # Assert that the limit:3 results are the first 3 results from the unlimited search resp = Couch.get(url, query: %{q: "some:field", limit: 3}) - assert resp.status_code == 200 - ids = get_ids(resp) - assert Enum.sort(ids) == Enum.sort(["bar:1", "bar:5", "bar:9"]) + assert_on_status(resp, 200, "Failed to search on non-partitioned dbs with the limit.") + assert List.starts_with?(expected_ids, get_ids(resp)) end @tag :with_db @@ -203,7 +210,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_design/library/_search/books" resp = Couch.get(url, query: %{q: "some:field", limit: 201}) - assert resp.status_code == 400 + assert_on_status(resp, 400, "Expected a failure on non-partitioned dbs with over limit.") end @tag :with_partitioned_db @@ -214,7 +221,7 @@ defmodule PartitionSearchTest do url = "/#{db_name}/_partition/foo/_design/library/_search/books" resp = Couch.post(url, body: %{q: "some:field", partition: "bar"}) - assert resp.status_code == 400 + assert_on_status(resp, 400, "Expected a failure on conflicting partition values.") end @tag :with_partitioned_db diff --git a/test/elixir/test/search_test.exs b/test/elixir/test/search_test.exs index edf08f30d5..ad5a13dbbb 100644 --- a/test/elixir/test/search_test.exs +++ b/test/elixir/test/search_test.exs @@ -1,5 +1,6 @@ defmodule SearchTest do use CouchTestCase + import Couch.Test.Asserts @moduletag :search @@ -17,7 +18,10 @@ defmodule SearchTest do %{"item" => "date", "place" => "lobby", "state" => "unknown", "price" => 1.25}, ]} ) - assert resp.status_code in [201, 202] + assert resp.status_code in [201, 202], + "Cannot create search docs. " <> + "Expected one of [201, 202], got: #{resp.status_code}, body: #{inspect resp.body}" + end def create_ddoc(db_name, opts \\ %{}) do @@ -40,7 +44,7 @@ defmodule SearchTest do ddoc = Enum.into(opts, default_ddoc) resp = Couch.put("/#{db_name}/_design/inventory", body: ddoc) - assert resp.status_code in [201, 202] + assert_on_status(resp, [201, 202], "Cannot create design doc.") assert Map.has_key?(resp.body, "ok") == true end @@ -54,7 +58,7 @@ defmodule SearchTest do ddoc = Enum.into(opts, invalid_ddoc) resp = Couch.put("/#{db_name}/_design/search", body: ddoc) - assert resp.status_code in [201, 202] + assert_on_status(resp, [201, 202], "Cannot create design doc.") assert Map.has_key?(resp.body, "ok") == true end @@ -71,7 +75,7 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.get(url, query: %{q: "*:*", include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") ids = get_items(resp) assert Enum.sort(ids) == Enum.sort(["apple", "banana", "carrot", "date"]) end @@ -84,7 +88,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.get(url, query: %{q: "*:*", drilldown: :jiffy.encode(["place", "kitchen"]), include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == Enum.sort(["apple", "banana", "carrot"]) end @@ -97,7 +102,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.get(url, query: %{q: "*:*", drilldown: :jiffy.encode(["state", "new", "unknown"]), include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == Enum.sort(["apple", "banana", "date"]) end @@ -110,7 +116,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.get(url, query: %{q: "*:*", drilldown: :jiffy.encode([["state", "old"], ["item", "apple"]]), include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == [] end @@ -123,7 +130,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits?q=*:*&drilldown=[\"state\",\"old\"]&drilldown=[\"item\",\"apple\"]&include_docs=true" resp = Couch.get(url) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == [] end @@ -137,7 +145,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.post(url, body: %{q: "*:*", include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == Enum.sort(["apple", "banana", "carrot", "date"]) end @@ -150,7 +159,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.post(url, body: %{query: "*:*", drilldown: ["place", "kitchen"], include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == Enum.sort(["apple", "banana", "carrot"]) end @@ -163,7 +173,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.post(url, body: %{query: "*:*", drilldown: ["state", "new", "unknown"], include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == Enum.sort(["apple", "banana", "date"]) end @@ -176,7 +187,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.post(url, body: %{q: "*:*", drilldown: [["state", "old"], ["item", "apple"]], include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == [] end @@ -189,7 +201,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.post(url, body: %{q: "*:*", drilldown: [["place", "kitchen"], ["state", "new"], ["item", "apple"]], include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == ["apple"] end @@ -202,7 +215,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.post(url, body: %{q: "*:*", drilldown: [["state", "old", "new"], ["item", "apple"]], include_docs: true}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == ["apple"] end @@ -215,7 +229,8 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" resp = Couch.post(url, body: "{\"include_docs\": true, \"q\": \"*:*\", \"drilldown\": [\"state\", \"old\"], \"drilldown\": [\"item\", \"apple\"]}") - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") + ids = get_items(resp) assert Enum.sort(ids) == ["apple"] end @@ -228,7 +243,7 @@ defmodule SearchTest do create_invalid_ddoc(db_name) resp = Couch.post("/#{db_name}/_search_cleanup") - assert resp.status_code in [201, 202] + assert_on_status(resp, [201, 202], "Fail to do a _search_cleanup.") end @tag :with_db @@ -240,7 +255,7 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" counts = ["place"] resp = Couch.get(url, query: %{q: "*:*", limit: 0, counts: :jiffy.encode(counts)}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") %{:body => %{"counts" => counts}} = resp assert counts == %{"place" => %{"kitchen" => 3, "lobby" => 1}} @@ -255,7 +270,7 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" counts = ["place"] resp = Couch.get(url, query: %{q: "item:tomato", limit: 0, counts: :jiffy.encode(counts)}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") %{:body => %{"counts" => counts}} = resp assert counts == %{"place" => %{}} @@ -270,7 +285,7 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" ranges = %{"price" => %{"cheap" => "[0 TO 0.99]", "expensive" => "[1.00 TO Infinity]"}} resp = Couch.get(url, query: %{q: "*:*", limit: 0, ranges: :jiffy.encode(ranges)}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") %{:body => %{"ranges" => ranges}} = resp assert ranges == %{"price" => %{"cheap" => 2, "expensive" => 2}} @@ -285,9 +300,37 @@ defmodule SearchTest do url = "/#{db_name}/_design/inventory/_search/fruits" ranges = %{"price" => %{}} resp = Couch.get(url, query: %{q: "*:*", limit: 0, ranges: :jiffy.encode(ranges)}) - assert resp.status_code == 200 + assert_on_status(resp, 200, "Fail to do search.") %{:body => %{"ranges" => ranges}} = resp assert ranges == %{"price" => %{}} end + + @tag :with_db + test "timeouts do not expose internal state", context do + db_name = context[:db_name] + create_search_docs(db_name) + create_ddoc(db_name) + + config = [ + %{ + :section => "fabric", + :key => "search_timeout", + :value => "0" + } + ] + + run_on_modified_server(config, fn -> + url = "/#{db_name}/_design/inventory/_search/fruits" + resp = Couch.get(url, query: %{q: "*:*", include_docs: true}) + assert resp.status_code == 500 + + %{ + :body => %{ + "error" => "timeout", + "reason" => "The request could not be processed in a reasonable amount of time." + } + } = resp + end) + end end From 449f5e2d728ad4cd123ebdab40bb5b4656e8d4a3 Mon Sep 17 00:00:00 2001 From: Nick Vatamaniuc Date: Thu, 30 Oct 2025 00:33:51 -0400 Subject: [PATCH 2/6] Bump version to 3.5.1 --- version.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.mk b/version.mk index 2a199c9ffc..fd4879ca04 100644 --- a/version.mk +++ b/version.mk @@ -1,3 +1,3 @@ vsn_major=3 vsn_minor=5 -vsn_patch=0 +vsn_patch=1 From 44f6a43d81b83b203a110f6036d107dc2afb5556 Mon Sep 17 00:00:00 2001 From: Nick Vatamaniuc Date: Thu, 30 Oct 2025 03:25:45 -0400 Subject: [PATCH 3/6] Doc updates for 3.5.1 Whatsnew and other fixes --- src/docs/src/config/misc.rst | 4 +- src/docs/src/whatsnew/3.5.rst | 145 ++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/src/docs/src/config/misc.rst b/src/docs/src/config/misc.rst index 6c9d6003e2..c5843680ba 100644 --- a/src/docs/src/config/misc.rst +++ b/src/docs/src/config/misc.rst @@ -66,7 +66,7 @@ UUIDs Configuration .. config:option:: algorithm :: Generation Algorithm .. versionchanged:: 1.3 Added ``utc_id`` algorithm. - .. versionchanged:: 3.6 Added ``uuid_v7`` algorithm. + .. versionchanged:: 3.5.1 Added ``uuid_v7`` algorithm. CouchDB provides various algorithms to generate the UUID values that are used for document `_id`'s by default:: @@ -317,7 +317,7 @@ Configuration of Database Purge revisions per Purge-Request .. versionadded:: 3.0 - .. versionchanged:: 3.6 + .. versionchanged:: 3.5.1 Sets the maximum number of accumulated revisions allowed in a single purge request:: diff --git a/src/docs/src/whatsnew/3.5.rst b/src/docs/src/whatsnew/3.5.rst index 02124d6110..b4c560f964 100644 --- a/src/docs/src/whatsnew/3.5.rst +++ b/src/docs/src/whatsnew/3.5.rst @@ -20,6 +20,151 @@ :depth: 1 :local: +.. _release/3.5.1: + +Version 3.5.1 +============= + +Features +-------- + +* :ghissue:`5626`, :ghissue:`5665`: Debian Trixie support +* :ghissue:`5709`: Automatic Nouveau and Clouseau index cleanup +* :ghissue:`5697`: Add UUID v7 as a ``uuid`` algorithm option. The default is + still the default ``sequential`` algorithm. +* :ghissue:`5713`, :ghissue:`5697`, :ghissue:`5701`, :ghissue:`5704`: Purge + improvements and fixes. Optimize it up to ~30% faster for large batches. + ``max_document_id_number`` setting was removed and ``max_revisions_number`` + set to ``unlimited`` by default to match ``_bulk_docs`` and ``_bulk_get`` + endpoints. +* :ghissue:`5611`: Implement the ability to downgrade CouchDB versions +* :ghissue:`5588`: Populate zone from ``COUCHDB_ZONE`` env variable in Docker +* :ghissue:`5563`: Set Erlang/OTP 26 as minimum supported version +* :ghissue:`5546`, :ghissue:`5641`: Improve Clouseau service checks in + ``clouseau_rpc`` module. +* :ghissue:`5639`: Use OS certificates for replication +* :ghissue:`5728`: Configurable reduce limit threshold and ratio + +Performance +----------- + +* :ghissue:`5625`: BTree engine term cache +* :ghissue:`5617`: Optimize Nouveau searches when index is fresh +* :ghissue:`5598`: Use HTTP/2 for Nouveau +* :ghissue:`5701`: Optimize revid parsing: 50-90% faster. Should help purge + requests as well as ``_bulk_docs`` and ``_bulk_get`` endpoints. +* :ghissue:`5564`: Use the built-in binary hex encode +* :ghissue:`5613`: Improve scanner performance +* :ghissue:`5545`: Bump process limit to 1M + +Bugfixes +-------- + +* :ghissue:`5722`, :ghissue:`5683`, :ghissue:`5678`, :ghissue:`5646`, + :ghissue:`5630`, :ghissue:`5615`, :ghissue:`5696`: Scanner fixes. Add write + limiting and switch to traversing documents by sequence IDs instead of by + document IDs. +* :ghissue:`5707`, :ghissue:`5706`, :ghissue:`5706`, :ghissue:`5694`, + :ghissue:`5691`, :ghissue:`5669`, :ghissue:`5629`, :ghissue:`5574`, + :ghissue:`5573`, :ghissue:`5566`, :ghissue:`5553`, :ghissue:`5550`, + :ghissue:`5534`, :ghissue:`5730`: QuickJS Updates. Optimized string operations, + faster context creation, a lot of bug fixes. +* :ghissue:`5719`: Use "all" ring options for purged_infos +* :ghissue:`5649`: Retry call to dreyfus index on noproc errors +* :ghissue:`5663`: More informative error if epochs out of order +* :ghissue:`5649`: Dreyfus retries on error +* :ghissue:`5643`: Fix reduce_limit = log feature +* :ghissue:`5620`: Use copy_props in the compactor instead of set_props +* :ghissue:`5632`, :ghissue:`5627`, :ghissue:`5607`: Nouveau fixes. Enhance + ``_nouveau_cleanup``. Improve security on http/2. +* :ghissue:`5614`: Stop replication jobs to nodes which are not part of the cluster +* :ghissue:`5596`: Fix query args parsing during cluster upgrades +* :ghissue:`5595`: Make replicator shutdown a bit more orderly +* :ghissue:`5595`: Avoid making a mess in the logs when stopping replicator app +* :ghissue:`5588`: Fix ``couch_util:set_value/3`` +* :ghissue:`5587`: Improve ``mem3_rep:find_source_seq/4`` logging +* :ghissue:`5586`: Don't wait indefinitely for replication jobs to stop +* :ghissue:`5578`: Use ``[sync]`` option in ``couch_bt_engine:commit_data/1`` +* :ghissue:`5556`: Add guards to ``fabric:design_docs/1`` to prevent + ``function_clause`` error +* :ghissue:`5555`: Improve replicator client mailbox flush +* :ghissue:`5551`: Handle ``bad_generator`` and ``case_clause`` in ``ken_server`` +* :ghissue:`5552`: Improve cluster startup logging +* :ghissue:`5552`: Improve mem3 supervisor +* :ghissue:`5552`: Handle shard opener tables not being initializes better +* :ghissue:`5549`: Don't spawn more than one ``init_delete_dir`` instance +* :ghissue:`5535`: Disk monitor always allows ``mem3_rep`` checkpoints +* :ghissue:`5536`: Fix ``mem3_util`` overlapping shards +* :ghissue:`5533`: No cfile support for 32bit systems +* :ghissue:`5688`: Handle timeout in ``dreyfus_fabric_search`` +* :ghissue:`5548`: Fix config key typo in mem3_reshard_dbdoc +* :ghissue:`5540`: Ignore extraneous cookie in replicator session plugin + +Cleanups +-------- + +* :ghissue:`5717`: Do not check for Dreyfus. It's part of the tree now. +* :ghissue:`5715`: Remove Hastings references +* :ghissue:`5714`: Cleanup fabric r/w parameter handling +* :ghissue:`5693`: Remove explicit erlang module prefix for auto-imported functions +* :ghissue:`5686`: Remove ``erlang:`` prefix from ``erlang:error()`` +* :ghissue:`5686`: Fix ``case_clause`` when got ``missing_target`` error +* :ghissue:`5690`: Fix props caching in mem3 +* :ghissue:`5680`: Implement db doc updating +* :ghissue:`5666`: Replace ``gen_server:format_status/2`` with ``format_status/1`` +* :ghissue:`5672`: Cache and store mem3 shard properties in one place only +* :ghissue:`5644`: Remove redundant ``*_to_list`` / ``list_to_*`` conversion +* :ghissue:`5633`: Use ``config:get_integer/3`` in couch_btree +* :ghissue:`5618`: DRY out ``couch_bt_engine`` header pointer term access +* :ghissue:`5614`: Stop replication jobs to nodes which are not part of the cluster +* :ghissue:`5610`: Add a ``range_to_hex/1`` utility function +* :ghissue:`5565`: Use maps comprehensions and generators in a few places +* :ghissue:`5649`: Remove pointless message +* :ghissue:`5649`: Remove obsolete clauses from dreyfus +* :ghissue:`5621`: Minor couch_btree refactoring + +Docs +---- + +* :ghissue:`5705`: Docs: Update the /_up endpoint docs to include status responses +* :ghissue:`5653`: Document that _all_dbs endpoint supports inclusive_end query param +* :ghissue:`5575`: Document how to mitigate high memory usage in docker +* :ghissue:`5600`: Avoid "master" wording at setup cluster +* :ghissue:`5381`: Change unauthorized example to 401 for replication +* :ghissue:`5682`: Update install instructions +* :ghissue:`5674`: Add setup documentation for two factor authentication +* :ghissue:`5562`: Add AI policy +* :ghissue:`5548`: Fix reshard doc section name +* :ghissue:`5543`: Add ``https`` to allowed replication proxy protocols + +Tests/CI/Builds +--------------- + +* :ghissue:`5720`: Update deps: Fauxton, meck and PropEr +* :ghissue:`5708`: Improve search test +* :ghissue:`5702`: Increase timeout for `process_response/3` to fix flaky tests +* :ghissue:`5703`: Use deterministic doc IDs in Mango key test +* :ghissue:`5692`: Implement 'assert_on_status' macro +* :ghissue:`5684`: Sequester docker ARM builds and fail early +* :ghissue:`5679`: Add ``--disable-spidermonkey`` to ``--dev[-with-nouveau]`` +* :ghissue:`5671`: Print request/response body on errors from mango test suite +* :ghissue:`5670`: Fix ``make clean`` after ``dev/run --enable-tls`` +* :ghissue:`5668`: Update xxHash +* :ghissue:`5667`: Update mochiweb to v3.3.0 +* :ghissue:`5664`: Disable ppc64le and s390x builds +* :ghissue:`5604`: Use ASF fork of ``gun`` for ``cowlib`` dependency +* :ghissue:`5636`: Reduce btree prop test count a bit +* :ghissue:`5633`: Fix and improve couch_btree testing +* :ghissue:`5572`: Remove a few more instances of Ubuntu Focal +* :ghissue:`5571`: Upgrade Erlang for CI +* :ghissue:`5570`: Skip macos CI for now and remove Ubuntu Focal +* :ghissue:`5488`: Bump Clouseau to 2.25.0 +* :ghissue:`5541`: Enable Clouseau for the Windows CI +* :ghissue:`5537`: Add retries to native full CI stage +* :ghissue:`5531`: Fix Erlang cookie configuration in ``dev/run`` +* :ghissue:`5662`: Remove old Jenkinsfiles +* :ghissue:`5661`: Unify CI jobs + .. _release/3.5.0: Version 3.5.0 From 08201e83a05a75af0bf84889e1ad814815f0bcac Mon Sep 17 00:00:00 2001 From: James Coglan Date: Fri, 9 Jan 2026 12:47:54 +0000 Subject: [PATCH 4/6] fix: Align vdu_rejects counter with actual VDU behaviour This counter is incremented whenever a VDU returns a value other than `1`, whereas `ok` and `true` are also treated as acceptable success values. This fixes the counter to only increment on actual failure responses. --- src/couch/src/couch_query_servers.erl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/couch/src/couch_query_servers.erl b/src/couch/src/couch_query_servers.erl index a204a767f9..1436501221 100644 --- a/src/couch/src/couch_query_servers.erl +++ b/src/couch/src/couch_query_servers.erl @@ -468,18 +468,18 @@ builtin_cmp_last(A, B) -> validate_doc_update(Db, DDoc, EditDoc, DiskDoc, Ctx, SecObj) -> JsonEditDoc = couch_doc:to_json_obj(EditDoc, [revs]), JsonDiskDoc = json_doc(DiskDoc), - Resp = ddoc_prompt( - Db, - DDoc, - [<<"validate_doc_update">>], - [JsonEditDoc, JsonDiskDoc, Ctx, SecObj] - ), - if - Resp == 1 -> ok; - true -> couch_stats:increment_counter([couchdb, query_server, vdu_rejects], 1) - end, + Args = [JsonEditDoc, JsonDiskDoc, Ctx, SecObj], + + Resp = + case ddoc_prompt(Db, DDoc, [<<"validate_doc_update">>], Args) of + Code when Code =:= 1; Code =:= ok; Code =:= true -> + ok; + Other -> + couch_stats:increment_counter([couchdb, query_server, vdu_rejects], 1), + Other + end, case Resp of - RespCode when RespCode =:= 1; RespCode =:= ok; RespCode =:= true -> + ok -> ok; {[{<<"forbidden">>, Message}]} -> throw({forbidden, Message}); From ede5be2e0f1d78047a30fc3f1e251942cbf8beb3 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 8 Jan 2026 15:19:47 +0000 Subject: [PATCH 5/6] chore: Add some basic testing for the JS-based VDU interface --- test/elixir/test/config/suite.elixir | 6 ++ test/elixir/test/validate_doc_update_test.exs | 79 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 test/elixir/test/validate_doc_update_test.exs diff --git a/test/elixir/test/config/suite.elixir b/test/elixir/test/config/suite.elixir index 1d1e6059a3..858421f1c0 100644 --- a/test/elixir/test/config/suite.elixir +++ b/test/elixir/test/config/suite.elixir @@ -513,6 +513,12 @@ "serial execution is not spuriously counted as loop on test_rewrite_suite_db", "serial execution is not spuriously counted as loop on test_rewrite_suite_db%2Fwith_slashes" ], + "ValidateDocUpdateTest": [ + "JavaScript VDU accepts a valid document", + "JavaScript VDU rejects an invalid document", + "JavaScript VDU accepts a valid change", + "JavaScript VDU rejects an invalid change", + ], "SecurityValidationTest": [ "Author presence and user security", "Author presence and user security when replicated", diff --git a/test/elixir/test/validate_doc_update_test.exs b/test/elixir/test/validate_doc_update_test.exs new file mode 100644 index 0000000000..5d15db1016 --- /dev/null +++ b/test/elixir/test/validate_doc_update_test.exs @@ -0,0 +1,79 @@ +defmodule ValidateDocUpdateTest do + use CouchTestCase + + @moduledoc """ + Test validate_doc_update behaviour + """ + + @js_type_check %{ + language: "javascript", + + validate_doc_update: ~s""" + function (newDoc) { + if (!newDoc.type) { + throw {forbidden: 'Documents must have a type field'}; + } + } + """ + } + + @tag :with_db + test "JavaScript VDU accepts a valid document", context do + db = context[:db_name] + Couch.put("/#{db}/_design/js-test", body: @js_type_check) + + resp = Couch.put("/#{db}/doc", body: %{"type" => "movie"}) + assert resp.status_code == 201 + assert resp.body["ok"] == true + end + + @tag :with_db + test "JavaScript VDU rejects an invalid document", context do + db = context[:db_name] + Couch.put("/#{db}/_design/js-test", body: @js_type_check) + + resp = Couch.put("/#{db}/doc", body: %{"not" => "valid"}) + assert resp.status_code == 403 + assert resp.body["error"] == "forbidden" + end + + @js_change_check %{ + language: "javascript", + + validate_doc_update: ~s""" + function (newDoc, oldDoc) { + if (oldDoc && newDoc.type !== oldDoc.type) { + throw {forbidden: 'Documents cannot change their type field'}; + } + } + """ + } + + @tag :with_db + test "JavaScript VDU accepts a valid change", context do + db = context[:db_name] + Couch.put("/#{db}/_design/js-test", body: @js_change_check) + + Couch.put("/#{db}/doc", body: %{"type" => "movie"}) + + doc = Couch.get("/#{db}/doc").body + updated = doc |> Map.merge(%{"type" => "movie", "title" => "Duck Soup"}) + resp = Couch.put("/#{db}/doc", body: updated) + + assert resp.status_code == 201 + end + + @tag :with_db + test "JavaScript VDU rejects an invalid change", context do + db = context[:db_name] + Couch.put("/#{db}/_design/js-test", body: @js_change_check) + + Couch.put("/#{db}/doc", body: %{"type" => "movie"}) + + doc = Couch.get("/#{db}/doc").body + updated = doc |> Map.put("type", "director") + resp = Couch.put("/#{db}/doc", body: updated) + + assert resp.status_code == 403 + end +end From cd883a20bed74b8642843432d05c8420729c4160 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Fri, 9 Jan 2026 14:05:43 +0000 Subject: [PATCH 6/6] feat: Add the ability for VDUs to be written as Mango selectors --- src/couch_mrview/src/couch_mrview.erl | 2 +- src/mango/src/mango_native_proc.erl | 27 ++++ test/elixir/test/config/suite.elixir | 6 + test/elixir/test/validate_doc_update_test.exs | 133 ++++++++++++++++++ 4 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/couch_mrview/src/couch_mrview.erl b/src/couch_mrview/src/couch_mrview.erl index bc7b1f8abf..244f668af0 100644 --- a/src/couch_mrview/src/couch_mrview.erl +++ b/src/couch_mrview/src/couch_mrview.erl @@ -62,7 +62,7 @@ validate_ddoc_fields(DDoc) -> [{<<"rewrites">>, [string, array]}], [{<<"shows">>, object}, {any, [object, string]}], [{<<"updates">>, object}, {any, [object, string]}], - [{<<"validate_doc_update">>, string}], + [{<<"validate_doc_update">>, [string, object]}], [{<<"views">>, object}, {<<"lib">>, object}], [{<<"views">>, object}, {any, object}, {<<"map">>, MapFuncType}], [{<<"views">>, object}, {any, object}, {<<"reduce">>, string}] diff --git a/src/mango/src/mango_native_proc.erl b/src/mango/src/mango_native_proc.erl index 511a987199..edcecd4b6f 100644 --- a/src/mango/src/mango_native_proc.erl +++ b/src/mango/src/mango_native_proc.erl @@ -29,6 +29,7 @@ -record(st, { indexes = [], + validators = [], timeout = 5000 }). @@ -94,6 +95,32 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) -> Else end, {reply, Vals, St}; +handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) -> + NewSt = + case couch_util:get_value(<<"validate_doc_update">>, DDoc) of + undefined -> + St; + Selector0 -> + Selector = mango_selector:normalize(Selector0), + Validators = couch_util:set_value(DDocId, St#st.validators, Selector), + St#st{validators = Validators} + end, + {reply, true, NewSt}; +handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _From, St) -> + case couch_util:get_value(DDocId, St#st.validators) of + undefined -> + Msg = [<<"validate_doc_update">>, DDocId], + {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}; + Selector -> + [NewDoc, OldDoc, _Ctx, _SecObj] = Args, + Struct = {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, OldDoc}]}, + Reply = + case mango_selector:match(Selector, Struct) of + true -> true; + _ -> {[{<<"forbidden">>, <<"document is not valid">>}]} + end, + {reply, Reply, St} + end; handle_call(Msg, _From, St) -> {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}. diff --git a/test/elixir/test/config/suite.elixir b/test/elixir/test/config/suite.elixir index 858421f1c0..9b5dacd756 100644 --- a/test/elixir/test/config/suite.elixir +++ b/test/elixir/test/config/suite.elixir @@ -518,6 +518,12 @@ "JavaScript VDU rejects an invalid document", "JavaScript VDU accepts a valid change", "JavaScript VDU rejects an invalid change", + "Mango VDU accepts a valid document", + "Mango VDU rejects an invalid document", + "updating a Mango VDU updates its effects", + "converting a Mango VDU to JavaScript updates its effects", + "deleting a Mango VDU removes its effects", + "Mango VDU rejects a doc if any existing ddoc fails to match", ], "SecurityValidationTest": [ "Author presence and user security", diff --git a/test/elixir/test/validate_doc_update_test.exs b/test/elixir/test/validate_doc_update_test.exs index 5d15db1016..93ed8f177c 100644 --- a/test/elixir/test/validate_doc_update_test.exs +++ b/test/elixir/test/validate_doc_update_test.exs @@ -76,4 +76,137 @@ defmodule ValidateDocUpdateTest do assert resp.status_code == 403 end + + @mango_type_check %{ + language: "query", + + validate_doc_update: %{ + "newDoc" => %{"type" => %{"$exists" => true}} + } + } + + @tag :with_db + test "Mango VDU accepts a valid document", context do + db = context[:db_name] + resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc", body: %{"type" => "movie"}) + assert resp.status_code == 201 + assert resp.body["ok"] == true + end + + @tag :with_db + test "Mango VDU rejects an invalid document", context do + db = context[:db_name] + resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc", body: %{"no" => "type"}) + assert resp.status_code == 403 + assert resp.body["error"] == "forbidden" + end + + @tag :with_db + test "updating a Mango VDU updates its effects", context do + db = context[:db_name] + + resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check) + assert resp.status_code == 201 + + ddoc = %{ + language: "query", + + validate_doc_update: %{ + "newDoc" => %{ + "type" => %{"$type" => "string"}, + "year" => %{"$lt" => 2026} + } + } + } + resp = Couch.put("/#{db}/_design/mango-test", body: ddoc, query: %{rev: resp.body["rev"]}) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc1", body: %{"type" => "movie", "year" => 1994}) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc2", body: %{"type" => 42, "year" => 1994}) + assert resp.status_code == 403 + assert resp.body["error"] == "forbidden" + + resp = Couch.put("/#{db}/doc3", body: %{"type" => "movie", "year" => 2094}) + assert resp.status_code == 403 + assert resp.body["error"] == "forbidden" + end + + @tag :with_db + test "converting a Mango VDU to JavaScript updates its effects", context do + db = context[:db_name] + + resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check) + assert resp.status_code == 201 + + ddoc = %{ + language: "javascript", + + validate_doc_update: ~s""" + function (newDoc) { + if (typeof newDoc.year !== 'number') { + throw {forbidden: 'Documents must have a valid year field'}; + } + } + """ + } + resp = Couch.put("/#{db}/_design/mango-test", body: ddoc, query: %{rev: resp.body["rev"]}) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc1", body: %{"year" => 1994}) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc2", body: %{"year" => "1994"}) + assert resp.status_code == 403 + assert resp.body["error"] == "forbidden" + end + + @tag :with_db + test "deleting a Mango VDU removes its effects", context do + db = context[:db_name] + + resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check) + assert resp.status_code == 201 + + resp = Couch.delete("/#{db}/_design/mango-test", query: %{rev: resp.body["rev"]}) + assert resp.status_code == 200 + + resp = Couch.put("/#{db}/doc", body: %{"no" => "type"}) + assert resp.status_code == 201 + end + + @tag :with_db + test "Mango VDU rejects a doc if any existing ddoc fails to match", context do + db = context[:db_name] + resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check) + assert resp.status_code == 201 + + ddoc = %{ + language: "query", + + validate_doc_update: %{ + "newDoc" => %{"year" => %{"$lt" => 2026}} + } + } + resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc1", body: %{"type" => "movie", "year" => 1994}) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc2", body: %{"year" => 1994}) + assert resp.status_code == 403 + assert resp.body["error"] == "forbidden" + + resp = Couch.put("/#{db}/doc3", body: %{"type" => "movie", "year" => 2094}) + assert resp.status_code == 403 + assert resp.body["error"] == "forbidden" + end end