harper 5.3.2 → 5.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +3 -3
- package/bin/copyDb.ts +4 -3
- package/components/DESIGN.md +12 -0
- package/components/packageComponent.ts +42 -17
- package/config/DESIGN.md +4 -0
- package/config/configUtils.ts +8 -8
- package/config/parseConfigFile.ts +11 -7
- package/dist/bin/cliOperations.js +3 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.js +3 -3
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/components/packageComponent.d.ts +9 -7
- package/dist/components/packageComponent.js +41 -16
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/config/configUtils.js +4 -3
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +1 -0
- package/dist/config/parseConfigFile.js +7 -3
- package/dist/config/parseConfigFile.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +80 -11
- package/dist/resources/DatabaseTransaction.js +1100 -281
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +1 -1
- package/dist/resources/LMDBTransaction.js +4 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/Resource.js +40 -14
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.js +1 -0
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.js +97 -34
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +34 -16
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/databases.d.ts +5 -0
- package/dist/resources/databases.js +38 -4
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/recordLock.d.ts +9 -4
- package/dist/resources/recordLock.js +79 -19
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -0
- package/dist/server/DurableSubscriptionsSession.js +24 -24
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/mqtt.js +59 -29
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.js +18 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +8 -2
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.js +3 -2
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -1
- package/dist/utility/npmUtilities.js +18 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/package.json +1 -1
- package/resources/DESIGN.md +46 -10
- package/resources/DatabaseTransaction.ts +1268 -279
- package/resources/LMDBTransaction.ts +4 -3
- package/resources/Resource.ts +39 -14
- package/resources/RocksTransactionLogStore.ts +1 -0
- package/resources/Table.ts +120 -41
- package/resources/analytics/DESIGN.md +17 -0
- package/resources/analytics/write.ts +28 -15
- package/resources/databases.ts +40 -4
- package/resources/record-locks.md +34 -17
- package/resources/recordLock.ts +89 -22
- package/server/DESIGN.md +9 -1
- package/server/DurableSubscriptionsSession.ts +22 -23
- package/server/mqtt.ts +54 -29
- package/server/serverHelpers/NodeAdapterResponse.ts +19 -1
- package/server/serverHelpers/contentTypes.ts +8 -1
- package/server/serverHelpers/sharedMessageEncoding.ts +3 -2
- package/studio/web/assets/Chat-DjZNWwwA.js +2100 -0
- package/studio/web/assets/FloatingChat-BWfKK_RX.js +23 -0
- package/studio/web/assets/SearchDialog-CdlNxWef.js +1 -0
- package/studio/web/assets/{abnfDiagram-O67JEVCF-raXE027J.js → abnfDiagram-YUKMZFIV-DPR2lFRk.js} +1 -1
- package/studio/web/assets/{apiToken-DNrfij0E.js → apiToken-DyI8IwU3.js} +1 -1
- package/studio/web/assets/applications-DgGopKJq.js +296 -0
- package/studio/web/assets/architecture-WOLXFQ4H-DtJHIK0_.js +1 -0
- package/studio/web/assets/{architectureDiagram-NJMV4G6O-C8_Eet7w.js → architectureDiagram-47P4ROYG-MDymp2dh.js} +3 -3
- package/studio/web/assets/authStore-DE1HFvVP.js +3 -0
- package/studio/web/assets/{blockDiagram-BEXU5L5S-DR6S5-1D.js → blockDiagram-E7TT5TSB-O5qRgZmd.js} +3 -3
- package/studio/web/assets/button-C37w80Uk.js +21 -0
- package/studio/web/assets/{c4Diagram-YGBWAQC7-BAfZ1G01.js → c4Diagram-CRA5TL53-BdPORGHo.js} +2 -2
- package/studio/web/assets/channel-yCStoq0_.js +1 -0
- package/studio/web/assets/{chevron-up-CFxbMGv2.js → chevron-up-DozwrIz-.js} +1 -1
- package/studio/web/assets/chunk-2BW5OAIV-DJ-iXJAw.js +10 -0
- package/studio/web/assets/{chunk-ZIGJFQKS-Cs8fUn7y.js → chunk-3YJQHVM4-BOgacqS1.js} +2 -2
- package/studio/web/assets/{chunk-7PRAP22T-KyqX6x3f.js → chunk-4EA7E6EY-C-7xqUBl.js} +1 -1
- package/studio/web/assets/{chunk-GWA4HPMP-DsejB6gA.js → chunk-7TKQ45FW-C3XA1bIN.js} +1 -1
- package/studio/web/assets/{chunk-GTNCS2PH-DGHLXyow.js → chunk-AW2ZBBNX-BdvgD1UW.js} +3 -3
- package/studio/web/assets/{chunk-5DYCD2WN-BceBfIHT.js → chunk-DBDB3WZW-CKKxIc6F.js} +1 -1
- package/studio/web/assets/{chunk-MBY4JIJT-BwSGweQP.js → chunk-E2ZNV5FY-bVefNHvz.js} +3 -3
- package/studio/web/assets/chunk-EU5HNXII-BHv2kWp4.js +213 -0
- package/studio/web/assets/chunk-FVRAUYC3-BoL_NO6b.js +2 -0
- package/studio/web/assets/{chunk-742MDFTN-CIkt8ijt.js → chunk-HJ2JQQFS-N_98oMRS.js} +1 -1
- package/studio/web/assets/{chunk-NETBCI7D-C8UnOw_f.js → chunk-HTAEGDNF-BQ1fSjPs.js} +1 -1
- package/studio/web/assets/{chunk-WEXAMYUT-BAIhTwyh.js → chunk-KQW6MTUR-qP0QiQAg.js} +1 -1
- package/studio/web/assets/chunk-NGNAAXSQ-DqoPQk1a.js +136 -0
- package/studio/web/assets/{chunk-UA2S7LBM-BHpEdH8D.js → chunk-NTY3LDVX-CSBFjlJg.js} +1 -1
- package/studio/web/assets/chunk-VPRB5NB3-BVHOe9Q-.js +129 -0
- package/studio/web/assets/{chunk-7INBJB4K-BpeAXyOi.js → chunk-XC4XBNZT-46uQwtF4.js} +10 -10
- package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DmDPkt2u.js → classDiagram-v2-K4WV4PDN-Da0ps1Ui.js} +3 -3
- package/studio/web/assets/cn-DpF1PxBT.js +1 -0
- package/studio/web/assets/{createLucideIcon-fO5dL7D5.js → createLucideIcon-DLWRoWN-.js} +1 -1
- package/studio/web/assets/{cssMode-DBucrQdT.js → cssMode-4ddO86ey.js} +1 -1
- package/studio/web/assets/cynefin-EF2NZ3EQ-jQ2eCUaU.js +1 -0
- package/studio/web/assets/cynefinDiagram-3GCD6N5R-TNi5iS0E.js +62 -0
- package/studio/web/assets/dagre-W4DXFKR2-BnLHmA44.js +4 -0
- package/studio/web/assets/{diagram-22UHCM2B-DJCvcaWP.js → diagram-2UJZ2QOL-r_jehhZR.js} +7 -7
- package/studio/web/assets/{diagram-3UASUU5V-Du6VKWln.js → diagram-OVF4WLC6-BSUXGVh8.js} +11 -11
- package/studio/web/assets/{diagram-CDSNMT55-l5Ga0u-W.js → diagram-PFPMY2P6-BrVedycw.js} +3 -3
- package/studio/web/assets/{diagram-MLGK6HIB-CNQY47LA.js → diagram-UMYRVEAY-CBIaakBB.js} +1 -1
- package/studio/web/assets/diagram-YEKJPTXX-DFmmDXCl.js +41 -0
- package/studio/web/assets/diagram-ZIFT7M5P-CQfOWxOi.js +3 -0
- package/studio/web/assets/dist-9lpzt2Hn.js +11 -0
- package/studio/web/assets/{ebnfDiagram-ZINNZB2B-5OnkTpfi.js → ebnfDiagram-VR2GEFS7-FXA3Xe6e.js} +1 -1
- package/studio/web/assets/{editor-CHpQCuBq.js → editor-DATuu7_v.js} +1 -1
- package/studio/web/assets/elk-IJKZMXRS-CC4gfVRU.js +27 -0
- package/studio/web/assets/{erDiagram-OPXOYQCR-CQlCX84D.js → erDiagram-O2IAPWRE-C-BkDTPl.js} +2 -2
- package/studio/web/assets/errorText-8xV8WZBt.js +1 -0
- package/studio/web/assets/eventmodeling-K75KTNOO-D0dJ_PbR.js +1 -0
- package/studio/web/assets/flowDiagram-OXPTDLAJ-C1LEc_Hf.js +1 -0
- package/studio/web/assets/{ganttDiagram-FUAMR5RP-bloX3Voy.js → ganttDiagram-R7TSEDQI-85F44BIo.js} +4 -4
- package/studio/web/assets/{getAnalytics-C0FHjtdQ.js → getAnalytics-B1xpd4kG.js} +1 -1
- package/studio/web/assets/gitGraph-VSP46ZUC-BBmqrmjK.js +1 -0
- package/studio/web/assets/gitGraphDiagram-XJZIOB7I-CFY6iMwg.js +106 -0
- package/studio/web/assets/graphlib-DS17s2tU.js +1 -0
- package/studio/web/assets/{html-B1iA8q38.js → html-c4OKnEF1.js} +1 -1
- package/studio/web/assets/{htmlMode-CrL3YO5Y.js → htmlMode-yXjqUsW9.js} +1 -1
- package/studio/web/assets/index-CDnn0-PN.css +1 -0
- package/studio/web/assets/index-CIK9Nhgo.js +833 -0
- package/studio/web/assets/index.lazy-DQpKL_QQ.js +1 -0
- package/studio/web/assets/info-OHQRW6UA-Bs2aEco7.js +1 -0
- package/studio/web/assets/{infoDiagram-VRGFBTTK-B0J1132p.js → infoDiagram-5W2HQ5XZ-d6yG1yfZ.js} +1 -1
- package/studio/web/assets/invalidateEntityQueries-BkWXiqjz.js +1 -0
- package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-BNk0tuQx.js → ishikawaDiagram-K3B6WC7H-CSYz-WRA.js} +2 -2
- package/studio/web/assets/{javascript-CXgQMkoa.js → javascript-CP1WPkLu.js} +1 -1
- package/studio/web/assets/{journeyDiagram-ZHPQQLJL-Be79RMQM.js → journeyDiagram-COXZFDF6-BDj6Wqcj.js} +3 -3
- package/studio/web/assets/{jsonMode-DycqRKOv.js → jsonMode-BWsIHqbb.js} +1 -1
- package/studio/web/assets/{kanban-definition-PNTS6WVX-4uuoQP9W.js → kanban-definition-P3RFRI5V-C2Bjfbp-.js} +6 -6
- package/studio/web/assets/{languageServices-HYHV1wDJ.js → languageServices-Ca72n4Ql.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-CH_6SYiA.js → lspLanguageFeatures-BQdsSDeh.js} +1 -1
- package/studio/web/assets/{mermaid-parser.core-V1UYMeXV.js → mermaid-parser.core-Ch_T61iL.js} +6 -6
- package/studio/web/assets/mermaid.core-BOAgKzo5.js +44 -0
- package/studio/web/assets/{mindmap-definition-NLK3R4M7-C70Ctm0J.js → mindmap-definition-LZFPQGKD-DzOapzMM.js} +3 -3
- package/studio/web/assets/notifications-BYgO5-yk.js +1 -0
- package/studio/web/assets/notifications-DsGk-TnW.js +1 -0
- package/studio/web/assets/operations-Dy69AlTr.js +4 -0
- package/studio/web/assets/packet-JDAUHWVQ-CGHB_EIE.js +1 -0
- package/studio/web/assets/{pegDiagram-GJSIUBJH-d6z1cc7W.js → pegDiagram-BGZESJAR-ADzOIKrz.js} +1 -1
- package/studio/web/assets/pie-XZMESJXO-Bijz5Y2o.js +1 -0
- package/studio/web/assets/pieDiagram-CAPJLFHJ-VM7fvbwO.js +39 -0
- package/studio/web/assets/profile-DzQvoYBF.js +1 -0
- package/studio/web/assets/{quadrantDiagram-O4NWA36T-CIF5Qkou.js → quadrantDiagram-GDMTTRAM-Bmza84_j.js} +3 -3
- package/studio/web/assets/radar-ABXABTNO-IaC8pB63.js +1 -0
- package/studio/web/assets/railroad-I3PHUGI6-CCrSUqeq.js +1 -0
- package/studio/web/assets/railroad-abnf-LNEFI6M7-BhobVp-t.js +1 -0
- package/studio/web/assets/railroad-ebnf-DWYD2UWJ-OHhGUTEh.js +1 -0
- package/studio/web/assets/railroad-peg-JR7BVNR7-FupC3Jcn.js +1 -0
- package/studio/web/assets/{railroadDiagram-XR7U4H2S-y3Jv2U6N.js → railroadDiagram-SM67HX2B-CgFGOVI5.js} +1 -1
- package/studio/web/assets/regions-Sv_IN7XA.js +1 -0
- package/studio/web/assets/{register-DCN4QUii.js → register-AM2_79FO.js} +3 -3
- package/studio/web/assets/request-BJj8dbdU.js +2 -0
- package/studio/web/assets/{requirementDiagram-PLB6GJNP-ChINZEmu.js → requirementDiagram-X7JNWC4A-BHoQgkoT.js} +4 -4
- package/studio/web/assets/restartGate-D_e93lFE.js +1 -0
- package/studio/web/assets/restartUnconfirmed-BqcDZ__Y.js +1 -0
- package/studio/web/assets/sankeyDiagram-UM26HJRW-8tFu_oUe.js +40 -0
- package/studio/web/assets/sequenceDiagram-ZO4K6R2Y-jryb1rwT.js +169 -0
- package/studio/web/assets/setComponentFile-BCnAnxjR.js +1 -0
- package/studio/web/assets/setup-Ca4RpPzm.js +2 -0
- package/studio/web/assets/{stateDiagram-v2-GCMORJYK-Bs8RIb9K.js → stateDiagram-v2-TBUQTH76-CnWE5yFn.js} +2 -2
- package/studio/web/assets/status-D05eZ5pQ.js +61 -0
- package/studio/web/assets/{swimlanes-2SLR337P-B52xrf6t.js → swimlanes-N4OXWK64-CBlFzDJn.js} +1 -1
- package/studio/web/assets/swimlanesDiagram-K3J5GTZL-DvFLtUPi.js +8 -0
- package/studio/web/assets/{timeline-definition-EJHVYXUP-DJSITpQX.js → timeline-definition-YOQAKGHF-CFWqHrqI.js} +4 -4
- package/studio/web/assets/{toggleHighContrast-CfAp6Sxb.js → toggleHighContrast-JRkLfyfb.js} +1 -1
- package/studio/web/assets/treeView-D4JQ5SDB-CpV9zuVb.js +1 -0
- package/studio/web/assets/treemap-SAJKECNS-Bc94Tqiq.js +1 -0
- package/studio/web/assets/{tsMode-HZR0bfq4.js → tsMode-C_-eSRVR.js} +1 -1
- package/studio/web/assets/{typescript-UMJSfZyF.js → typescript-BYXigM_n.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-B379CS-h.js → useEntityRestURL-HHmjsRm9.js} +1 -1
- package/studio/web/assets/usecaseDiagram-VIAY4XPW-Brf2xP3S.js +328 -0
- package/studio/web/assets/{vendor-core-6LhfJlRC.js → vendor-core-PejxhUWQ.js} +1 -1
- package/studio/web/assets/vendor-datadog-GjET_Cbn.js +6 -0
- package/studio/web/assets/{vendor-react-BOliS0BL.js → vendor-react-DSIx92Ld.js} +1 -1
- package/studio/web/assets/vendor-tanstack-C5Y-nrd2.js +2 -0
- package/studio/web/assets/vendor-ui-BBWHOEoQ.js +41 -0
- package/studio/web/assets/{vennDiagram-UO4OBE2U-DZTsDA4k.js → vennDiagram-BLWOH2XV-XxOyJPq_.js} +3 -3
- package/studio/web/assets/wardley-7MLQ67FV-DRW0zuE4.js +1 -0
- package/studio/web/assets/{wardleyDiagram-VNRHLVJA-qu_f7Tvl.js → wardleyDiagram-YMQ3BMBF-B3WskJ9W.js} +3 -3
- package/studio/web/assets/wasAReleasedBeforeB-D5grAaGM.js +1 -0
- package/studio/web/assets/{workers-Bn_0_L1R.js → workers-DY0ka4Y7.js} +1 -1
- package/studio/web/assets/{xml-CG2N-1r1.js → xml-CdGINpmN.js} +1 -1
- package/studio/web/assets/xychartDiagram-TAQBALBS-D5NV4Env.js +7 -0
- package/studio/web/assets/{yaml-68pda6Av.js → yaml-DYeF07_w.js} +1 -1
- package/studio/web/index.html +17 -15
- package/utility/npmUtilities.ts +17 -7
- package/studio/web/assets/Chat-CrxPD4ph.js +0 -2294
- package/studio/web/assets/FloatingChat-EXzoUQ-9.js +0 -23
- package/studio/web/assets/SearchDialog-7qRL8Yqx.js +0 -1
- package/studio/web/assets/_basePickBy-QJXUw_pr.js +0 -1
- package/studio/web/assets/_baseUniq-DR4vVjyC.js +0 -1
- package/studio/web/assets/alertDialog-BbWD9Xxa.js +0 -1
- package/studio/web/assets/applications-CdqjiSQe.js +0 -296
- package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +0 -1
- package/studio/web/assets/arrow-up-D2g_57vT.js +0 -1
- package/studio/web/assets/authStore-CJCaADw9.js +0 -3
- package/studio/web/assets/button-DWwfgGWI.js +0 -21
- package/studio/web/assets/channel-D0AkPjJ8.js +0 -1
- package/studio/web/assets/chunk-3FUC2YCW-DuRp-j19.js +0 -2
- package/studio/web/assets/chunk-7M6MHVWA-h6WlaTzO.js +0 -213
- package/studio/web/assets/chunk-FOHPRMQF-DzxwRta7.js +0 -161
- package/studio/web/assets/chunk-O7XYJQB3-CBS3nZYV.js +0 -128
- package/studio/web/assets/chunk-Z7XXMR3K-DSHbImdf.js +0 -10
- package/studio/web/assets/cn-6T8VranJ.js +0 -1
- package/studio/web/assets/cynefin-OW5HDTMX-DIvbXLSj.js +0 -1
- package/studio/web/assets/cynefinDiagram-VND7K2PF-BdtaXiCE.js +0 -62
- package/studio/web/assets/dagre-6A5THRUB-C7RUWHyz.js +0 -4
- package/studio/web/assets/diagram-ATOU4E4O-dxRfeG4B.js +0 -3
- package/studio/web/assets/diagram-MPIPVDR6-7prveD1B.js +0 -41
- package/studio/web/assets/dist-BrRuMUzt.js +0 -11
- package/studio/web/assets/download-Cn6tPG4L.js +0 -1
- package/studio/web/assets/elk-276RUBZZ-DxDQPjQT.js +0 -27
- package/studio/web/assets/errorText-NpBmvk9I.js +0 -1
- package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +0 -1
- package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +0 -1
- package/studio/web/assets/gitGraph-4MIJSDKK-BrXWmJt2.js +0 -1
- package/studio/web/assets/gitGraphDiagram-X574FWY7-C2KnnZeY.js +0 -106
- package/studio/web/assets/graphlib-C9Tl7QrK.js +0 -1
- package/studio/web/assets/index-QLZw6xrU.css +0 -1
- package/studio/web/assets/index-qCLyhyah.js +0 -828
- package/studio/web/assets/index.lazy-DCx-DxIV.js +0 -2
- package/studio/web/assets/info-A6RAGUB7-BhzBHe4U.js +0 -1
- package/studio/web/assets/mermaid.core-1PX6R49U.js +0 -44
- package/studio/web/assets/notifications-BSObVIY1.js +0 -1
- package/studio/web/assets/notifications-DFLXk68b.js +0 -1
- package/studio/web/assets/packet-AYTQ26CC-DWbXHx5g.js +0 -1
- package/studio/web/assets/pie-WAS4IAKB-DjoTm3yr.js +0 -1
- package/studio/web/assets/pieDiagram-5QR66LMP-Dz3RHpDA.js +0 -39
- package/studio/web/assets/profile-TBjzOMIy.js +0 -1
- package/studio/web/assets/radar-RG4KPBEZ-CVBgjCOj.js +0 -1
- package/studio/web/assets/railroad-74A4TZTK-BdhXw10Q.js +0 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +0 -1
- package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +0 -1
- package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +0 -1
- package/studio/web/assets/regions-C4an4H7e.js +0 -1
- package/studio/web/assets/restartGate-nHAFLDh-.js +0 -1
- package/studio/web/assets/sankeyDiagram-IPEJSGJF-D9AyLce2.js +0 -40
- package/studio/web/assets/sequenceDiagram-PO4LG4MO-hfk4aIEm.js +0 -169
- package/studio/web/assets/setComponentFile-BrqOM1Zm.js +0 -1
- package/studio/web/assets/setup-0v2MCm-E.js +0 -2
- package/studio/web/assets/status-DdezofUu.js +0 -61
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +0 -8
- package/studio/web/assets/tabs-BCTQzoQK.js +0 -1
- package/studio/web/assets/treeView-Q6P3EWNA-C-epdxRo.js +0 -1
- package/studio/web/assets/treemap-WGGIJYW6-DOVyp_zw.js +0 -1
- package/studio/web/assets/usecaseDiagram-POWQR4AR-B6252cbq.js +0 -328
- package/studio/web/assets/vendor-datadog-ozKf-pts.js +0 -6
- package/studio/web/assets/vendor-tanstack-Bntfka-M.js +0 -2
- package/studio/web/assets/vendor-ui-9EYYDgoN.js +0 -41
- package/studio/web/assets/wardley-WFR3VGLG-Cfqoe6z4.js +0 -1
- package/studio/web/assets/xychartDiagram-PMCCYNJV-UrOKRsmR.js +0 -7
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.RELEASED_TRANSACTION = exports.ImmediateTransaction = exports.DatabaseTransaction = exports.COMMIT_PHASE_GRACE = exports.TRANSACTION_STATE = void 0;
|
|
37
|
+
exports.resetStuckCommitLogCooldownForTests = resetStuckCommitLogCooldownForTests;
|
|
37
38
|
exports.trackOutstandingCommit = trackOutstandingCommit;
|
|
38
39
|
exports.databaseCommitsSuspended = databaseCommitsSuspended;
|
|
39
40
|
exports.permanentlySuspendDatabaseCommits = permanentlySuspendDatabaseCommits;
|
|
@@ -61,7 +62,9 @@ exports.isJoinableScope = isJoinableScope;
|
|
|
61
62
|
exports.deferForCommitInFlight = deferForCommitInFlight;
|
|
62
63
|
exports.resetReplayedWritesWarning = resetReplayedWritesWarning;
|
|
63
64
|
exports.setMaxOutstandingTxnDuration = setMaxOutstandingTxnDuration;
|
|
65
|
+
exports.setMaxRetryDelayMs = setMaxRetryDelayMs;
|
|
64
66
|
exports.isWriteSupervised = isWriteSupervised;
|
|
67
|
+
exports.getTxnExpiration = getTxnExpiration;
|
|
65
68
|
exports.setTxnExpiration = setTxnExpiration;
|
|
66
69
|
const blob_ts_1 = require("./blob.js");
|
|
67
70
|
const commonUtility_ts_1 = require("../utility/lmdb/commonUtility.js");
|
|
@@ -107,7 +110,7 @@ const MAX_RETRIES = 40;
|
|
|
107
110
|
exports.COMMIT_PHASE_GRACE = 10;
|
|
108
111
|
// Cap the per-retry backoff so replication-applied transactions, which retry conflicts without a
|
|
109
112
|
// cap (see the commit rejection handler), don't grow the delay unbounded.
|
|
110
|
-
|
|
113
|
+
let MAX_RETRY_DELAY_MS = 1000;
|
|
111
114
|
let oldestOutstandingCommit;
|
|
112
115
|
let newestOutstandingCommit;
|
|
113
116
|
let outstandingCommitCount = 0;
|
|
@@ -123,13 +126,21 @@ const OVERLOAD_LOG_MIN_INTERVAL_MS = 1000;
|
|
|
123
126
|
// wedge and would starve `abandon`, which names a different transaction and is the only server-side
|
|
124
127
|
// record of why a request was failed. Mutates only when it grants, so a caller with its own
|
|
125
128
|
// suppression as well (checkOverloaded's per-node `logged`) must test that first.
|
|
126
|
-
const lastStuckCommitLogAt = { shed: -Infinity, abandon: -Infinity };
|
|
129
|
+
const lastStuckCommitLogAt = { shed: -Infinity, abandon: -Infinity, overdue: -Infinity };
|
|
127
130
|
function allowStuckCommitLog(site, now) {
|
|
128
131
|
if (now - lastStuckCommitLogAt[site] <= OVERLOAD_LOG_MIN_INTERVAL_MS)
|
|
129
132
|
return false;
|
|
130
133
|
lastStuckCommitLogAt[site] = now;
|
|
131
134
|
return true;
|
|
132
135
|
}
|
|
136
|
+
/** Test seam: clears the stuck- and stalled-commit log cooldowns so consecutive tests can assert on log emission without sleeping. */
|
|
137
|
+
function resetStuckCommitLogCooldownForTests() {
|
|
138
|
+
lastStuckCommitLogAt.shed = -Infinity;
|
|
139
|
+
lastStuckCommitLogAt.abandon = -Infinity;
|
|
140
|
+
lastStuckCommitLogAt.overdue = -Infinity;
|
|
141
|
+
lastStalledCommitWarningAt = -Infinity;
|
|
142
|
+
lastStalledCommitErrorAt = -Infinity;
|
|
143
|
+
}
|
|
133
144
|
// Which database/table, which native transaction, and which request a stuck commit belongs to —
|
|
134
145
|
// without it a wedge gives no indication of what to investigate (harper#2001).
|
|
135
146
|
function describeCommitIdentity(store, startedFrom, nativeTransaction, rootStore = store?.rootStore) {
|
|
@@ -330,14 +341,23 @@ let replayedWritesWarned = false;
|
|
|
330
341
|
* committed or aborted, and every caller is a cleanup path whose own callers have no handler — a
|
|
331
342
|
* throw there would abandon the rest of the cleanup.
|
|
332
343
|
*/
|
|
333
|
-
function abortNativeTransaction(transaction, context) {
|
|
344
|
+
function abortNativeTransaction(transaction, context, store, startedFrom) {
|
|
334
345
|
if (transaction == null)
|
|
335
346
|
return;
|
|
347
|
+
const txn = transaction;
|
|
348
|
+
if (txn.abortedByWrapper || txn.committed || txn.commitInFlight)
|
|
349
|
+
return;
|
|
350
|
+
txn.abortedByWrapper = true;
|
|
336
351
|
try {
|
|
337
352
|
transaction.abort();
|
|
338
353
|
}
|
|
339
354
|
catch (error) {
|
|
340
|
-
|
|
355
|
+
if (error?.code === 'ERR_TRANSACTION_ABANDONED') {
|
|
356
|
+
harperLogger.error?.(`Aborted a transaction with a durable log entry (${context}), ${describeCommitIdentity(store, startedFrom, transaction)}`, error);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
harperLogger.debug?.(context, error);
|
|
360
|
+
}
|
|
341
361
|
}
|
|
342
362
|
}
|
|
343
363
|
// The analytics module registers a recorder here at load (dependency inversion, mirroring
|
|
@@ -564,6 +584,23 @@ function clearAttemptState(txn) {
|
|
|
564
584
|
txn.postSubmitPoisoned = false;
|
|
565
585
|
txn.stalledCommitResourcesReleased = false;
|
|
566
586
|
}
|
|
587
|
+
// Returned by takeCompletions() when nothing is staged; only its length is read before Promise.all.
|
|
588
|
+
const NO_COMPLETIONS = Object.freeze([]);
|
|
589
|
+
// Crash-recovery replay sets `isRetry` before `save()`, so `hasStagedLog` is never set there.
|
|
590
|
+
function hasDurableLog(transaction, error) {
|
|
591
|
+
const txn = transaction;
|
|
592
|
+
return error?.hasLog === true || Boolean(txn?.isRetry && txn?.hasStagedLog);
|
|
593
|
+
}
|
|
594
|
+
function releaseTransactionLockHolds(transaction) {
|
|
595
|
+
const txn = transaction;
|
|
596
|
+
const holds = txn?.lockHolds;
|
|
597
|
+
if (!holds)
|
|
598
|
+
return;
|
|
599
|
+
txn.lockHolds = undefined;
|
|
600
|
+
for (const handle of holds) {
|
|
601
|
+
handle.releaseCommitHold?.();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
567
604
|
class DatabaseTransaction {
|
|
568
605
|
#context;
|
|
569
606
|
// Whether a resources/transaction.ts scope owns this instance — i.e. a final commit or abort is
|
|
@@ -583,6 +620,10 @@ class DatabaseTransaction {
|
|
|
583
620
|
readTxn;
|
|
584
621
|
readTxnRefCount;
|
|
585
622
|
readTxnsUsed;
|
|
623
|
+
// Bumped each time this instance acquires a read handle, so a reference can be returned to the
|
|
624
|
+
// acquisition it came from (doneReadTxn()). Not the handle's identity: lmdb-js hands back the same
|
|
625
|
+
// read Txn object for a new snapshot.
|
|
626
|
+
readTxnGeneration = 0;
|
|
586
627
|
timeout;
|
|
587
628
|
writeTick = -1;
|
|
588
629
|
timeoutBudget = 0;
|
|
@@ -705,6 +746,7 @@ class DatabaseTransaction {
|
|
|
705
746
|
readTransactionOwners.set(transaction, this);
|
|
706
747
|
this.rangeReadActive = false;
|
|
707
748
|
this.readTxnsUsed = 1;
|
|
749
|
+
this.readTxnGeneration++;
|
|
708
750
|
this.baseReadRefConsumed = false;
|
|
709
751
|
this.handleOpenedAt = performance.now();
|
|
710
752
|
}
|
|
@@ -806,11 +848,12 @@ class DatabaseTransaction {
|
|
|
806
848
|
// is not worth doing either, so those reads stay untraced.
|
|
807
849
|
if (DEBUG_LONG_TXNS && this.stackTraces)
|
|
808
850
|
this.stackTraces.push(new StartedTransaction());
|
|
809
|
-
|
|
851
|
+
if (readTxn)
|
|
852
|
+
this.readTxnsUsed++;
|
|
810
853
|
return readTxn;
|
|
811
854
|
}
|
|
812
|
-
doneReadTxn() {
|
|
813
|
-
if (!this.transaction)
|
|
855
|
+
doneReadTxn(generation = this.readTxnGeneration) {
|
|
856
|
+
if (!this.transaction || generation !== this.readTxnGeneration)
|
|
814
857
|
return;
|
|
815
858
|
if (--this.readTxnsUsed === 0) {
|
|
816
859
|
// The native handle was only being held open for the read iterators: any writes staged
|
|
@@ -851,7 +894,7 @@ class DatabaseTransaction {
|
|
|
851
894
|
*/
|
|
852
895
|
releaseReadTxn() {
|
|
853
896
|
const transaction = this.detachOwnedTransaction();
|
|
854
|
-
abortNativeTransaction(transaction, 'releasing timed-out read transaction');
|
|
897
|
+
abortNativeTransaction(transaction, 'releasing timed-out read transaction', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
855
898
|
this.completeDeferredContextRelease();
|
|
856
899
|
}
|
|
857
900
|
/**
|
|
@@ -889,7 +932,7 @@ class DatabaseTransaction {
|
|
|
889
932
|
let writesForStore = (this.writesByKey ??= new Map()).get(operation.store);
|
|
890
933
|
if (!writesForStore)
|
|
891
934
|
this.writesByKey.set(operation.store, (writesForStore = new Map()));
|
|
892
|
-
const keyId = writeKeyId(operation.key);
|
|
935
|
+
const keyId = (operation.keyId = writeKeyId(operation.key));
|
|
893
936
|
const priorWrite = writesForStore.get(keyId);
|
|
894
937
|
if (priorWrite)
|
|
895
938
|
operation.priorWrite = priorWrite;
|
|
@@ -904,6 +947,8 @@ class DatabaseTransaction {
|
|
|
904
947
|
if (index > -1)
|
|
905
948
|
this.writes[index] = null;
|
|
906
949
|
this.ownedWrites?.delete(operation);
|
|
950
|
+
if (operation.stagedIn === this)
|
|
951
|
+
operation.stagedIn = undefined;
|
|
907
952
|
if (operation.key === undefined)
|
|
908
953
|
return;
|
|
909
954
|
const writesForStore = this.writesByKey?.get(operation.store);
|
|
@@ -991,12 +1036,299 @@ class DatabaseTransaction {
|
|
|
991
1036
|
if (recordLocks.size === 0)
|
|
992
1037
|
this.recordLocks = undefined;
|
|
993
1038
|
}
|
|
994
|
-
|
|
1039
|
+
budgetCarrier(transaction) {
|
|
1040
|
+
const txn = transaction;
|
|
1041
|
+
if (txn?.commitStartedAt != null)
|
|
1042
|
+
return txn;
|
|
1043
|
+
if (this.saveCommits && txn && this.hasSiblingCommitInFlight(transaction ?? undefined)) {
|
|
1044
|
+
return txn;
|
|
1045
|
+
}
|
|
1046
|
+
if (txn?.immediateCommit)
|
|
1047
|
+
return txn;
|
|
1048
|
+
return this.saveCommits ? this : (this.root ?? this);
|
|
1049
|
+
}
|
|
1050
|
+
backfillActiveHandleWrites(handle) {
|
|
1051
|
+
if (handle.immediateCommit)
|
|
1052
|
+
return;
|
|
1053
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1054
|
+
const w = this.writes[i];
|
|
1055
|
+
if (w?.saved && !w.stagedOn)
|
|
1056
|
+
w.stagedOn = handle;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
isSingleHandleTarget(txn) {
|
|
1060
|
+
return (!txn.immediateCommit &&
|
|
1061
|
+
(!this.activeCommitHandles || this.activeCommitHandles.size === 0) &&
|
|
1062
|
+
(!this.activeCommitHandle || this.activeCommitHandle === txn));
|
|
1063
|
+
}
|
|
1064
|
+
getHandleStagedTxnTime(txn) {
|
|
1065
|
+
if (!txn)
|
|
1066
|
+
return undefined;
|
|
1067
|
+
if (this.isSingleHandleTarget(txn) &&
|
|
1068
|
+
(this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
|
|
1069
|
+
return this.activeHandleStagedTxnTime || txn.stagedTxnTime;
|
|
1070
|
+
}
|
|
1071
|
+
return txn.stagedTxnTime;
|
|
1072
|
+
}
|
|
1073
|
+
setHandleStagedTxnTime(txn, txnTime) {
|
|
1074
|
+
if (this.isSingleHandleTarget(txn) &&
|
|
1075
|
+
(this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
|
|
1076
|
+
if (!this.activeHandleStagedTxnTime)
|
|
1077
|
+
this.activeHandleStagedTxnTime = txnTime;
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
txn.stagedTxnTime ??= txnTime;
|
|
1081
|
+
}
|
|
1082
|
+
isHandleCommitInFlight(txn) {
|
|
1083
|
+
if (!txn)
|
|
1084
|
+
return false;
|
|
1085
|
+
if (this.activeCommitHandle === txn) {
|
|
1086
|
+
return Boolean(this.activeHandleInFlight || txn.commitInFlight);
|
|
1087
|
+
}
|
|
1088
|
+
return Boolean(txn.commitInFlight);
|
|
1089
|
+
}
|
|
1090
|
+
setHandleCommitInFlight(txn, inFlight) {
|
|
1091
|
+
if (this.isSingleHandleTarget(txn) && inFlight && !this.activeCommitHandle) {
|
|
1092
|
+
this.activeCommitHandle = txn;
|
|
1093
|
+
}
|
|
1094
|
+
if (this.activeCommitHandle === txn) {
|
|
1095
|
+
this.activeHandleInFlight = inFlight;
|
|
1096
|
+
if (!inFlight) {
|
|
1097
|
+
txn.commitInFlight = false;
|
|
1098
|
+
}
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
txn.commitInFlight = inFlight;
|
|
1102
|
+
}
|
|
1103
|
+
isHandleCommitted(txn) {
|
|
1104
|
+
if (!txn)
|
|
1105
|
+
return false;
|
|
1106
|
+
if (this.activeCommitHandle === txn) {
|
|
1107
|
+
return Boolean(this.activeHandleCommitted || txn.committed);
|
|
1108
|
+
}
|
|
1109
|
+
return Boolean(txn.committed);
|
|
1110
|
+
}
|
|
1111
|
+
setHandleCommitted(txn) {
|
|
1112
|
+
if (this.activeCommitHandle === txn) {
|
|
1113
|
+
this.activeHandleInFlight = false;
|
|
1114
|
+
this.activeHandleCommitted = true;
|
|
1115
|
+
txn.commitInFlight = false;
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
txn.commitInFlight = false;
|
|
1119
|
+
txn.committed = true;
|
|
1120
|
+
}
|
|
1121
|
+
trackActiveCommitHandle(transaction) {
|
|
1122
|
+
if (!transaction.immediateCommit &&
|
|
1123
|
+
!this.activeCommitHandle &&
|
|
1124
|
+
(!this.activeCommitHandles || this.activeCommitHandles.size === 0)) {
|
|
1125
|
+
this.activeCommitHandle = transaction;
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (this.activeCommitHandle === transaction)
|
|
1129
|
+
return;
|
|
1130
|
+
let handles = this.activeCommitHandles;
|
|
1131
|
+
if (!handles) {
|
|
1132
|
+
handles = this.activeCommitHandles = new Set();
|
|
1133
|
+
}
|
|
1134
|
+
if (this.activeCommitHandle) {
|
|
1135
|
+
const first = this.activeCommitHandle;
|
|
1136
|
+
if (this.activeHandleStagedTxnTime && !first.stagedTxnTime) {
|
|
1137
|
+
first.stagedTxnTime = this.activeHandleStagedTxnTime;
|
|
1138
|
+
}
|
|
1139
|
+
if (this.activeHandleInFlight)
|
|
1140
|
+
first.commitInFlight = true;
|
|
1141
|
+
if (this.activeHandleCommitted)
|
|
1142
|
+
first.committed = true;
|
|
1143
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1144
|
+
this.activeHandleInFlight = false;
|
|
1145
|
+
this.activeHandleCommitted = false;
|
|
1146
|
+
this.backfillActiveHandleWrites(first);
|
|
1147
|
+
if (this.saveCommits) {
|
|
1148
|
+
if (this.commitStartedAt != null && first.commitStartedAt == null) {
|
|
1149
|
+
first.commitStartedAt = this.commitStartedAt;
|
|
1150
|
+
this.commitStartedAt = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
if (this.commitOverdueLogged) {
|
|
1153
|
+
first.commitOverdueLogged = true;
|
|
1154
|
+
this.commitOverdueLogged = undefined;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
handles.add(this.activeCommitHandle);
|
|
1158
|
+
this.activeCommitHandle = undefined;
|
|
1159
|
+
}
|
|
1160
|
+
handles.add(transaction);
|
|
1161
|
+
}
|
|
1162
|
+
releaseActiveCommitHandle(transaction) {
|
|
1163
|
+
if (this.activeCommitHandle === transaction) {
|
|
1164
|
+
this.activeCommitHandle = undefined;
|
|
1165
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1166
|
+
this.activeHandleInFlight = false;
|
|
1167
|
+
this.activeHandleCommitted = false;
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const handles = this.activeCommitHandles;
|
|
1171
|
+
if (!handles)
|
|
1172
|
+
return;
|
|
1173
|
+
handles.delete(transaction);
|
|
1174
|
+
if (handles.size === 1) {
|
|
1175
|
+
for (const remaining of handles) {
|
|
1176
|
+
const rem = remaining;
|
|
1177
|
+
if (!rem.immediateCommit) {
|
|
1178
|
+
this.activeCommitHandle = remaining;
|
|
1179
|
+
this.activeHandleStagedTxnTime = rem.stagedTxnTime ?? 0;
|
|
1180
|
+
this.activeHandleInFlight = Boolean(rem.commitInFlight);
|
|
1181
|
+
this.activeHandleCommitted = Boolean(rem.committed);
|
|
1182
|
+
handles.clear();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
clearActiveCommitHandles() {
|
|
1188
|
+
if (this.activeCommitHandle !== undefined || this.activeHandleStagedTxnTime) {
|
|
1189
|
+
this.activeCommitHandle = undefined;
|
|
1190
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1191
|
+
this.activeHandleInFlight = false;
|
|
1192
|
+
this.activeHandleCommitted = false;
|
|
1193
|
+
}
|
|
1194
|
+
this.activeCommitHandles?.clear();
|
|
1195
|
+
}
|
|
1196
|
+
hasActiveCommitHandle() {
|
|
1197
|
+
return (this.activeCommitHandle !== undefined ||
|
|
1198
|
+
(this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0));
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Retries so far of the handles with a durable transaction-log entry still retrying their commit on
|
|
1202
|
+
* this link, summed over overlapping commits so the total moves whenever any of them does, or -1
|
|
1203
|
+
* when there is none.
|
|
1204
|
+
*/
|
|
1205
|
+
activeDurableLogRetries() {
|
|
1206
|
+
let retries = -1;
|
|
1207
|
+
const note = (handle) => {
|
|
1208
|
+
const txn = handle;
|
|
1209
|
+
if (!txn || !hasDurableLog(txn) || this.isHandleCommitted(txn) || txn.abortedByWrapper)
|
|
1210
|
+
return;
|
|
1211
|
+
retries = Math.max(retries, 0) + (txn.retries ?? 0);
|
|
1212
|
+
};
|
|
1213
|
+
note(this.activeCommitHandle);
|
|
1214
|
+
if (this.activeCommitHandles)
|
|
1215
|
+
for (const handle of this.activeCommitHandles)
|
|
1216
|
+
note(handle);
|
|
1217
|
+
note(this.transaction);
|
|
1218
|
+
return retries;
|
|
1219
|
+
}
|
|
1220
|
+
isActiveCommitHandle(transaction) {
|
|
1221
|
+
return this.activeCommitHandle === transaction || Boolean(this.activeCommitHandles?.has(transaction));
|
|
1222
|
+
}
|
|
1223
|
+
hasSiblingCommitInFlight(targetTransaction) {
|
|
1224
|
+
const excluded = targetTransaction ?? this.transaction;
|
|
1225
|
+
if (this.activeCommitHandle)
|
|
1226
|
+
return this.activeCommitHandle !== excluded;
|
|
1227
|
+
const active = this.activeCommitHandles;
|
|
1228
|
+
if (!active || active.size === 0)
|
|
1229
|
+
return false;
|
|
1230
|
+
if (!excluded)
|
|
1231
|
+
return true;
|
|
1232
|
+
return active.size > (active.has(excluded) ? 1 : 0);
|
|
1233
|
+
}
|
|
1234
|
+
isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
|
|
1235
|
+
if (!write)
|
|
1236
|
+
return false;
|
|
1237
|
+
if (targetTransaction?.immediateCommit) {
|
|
1238
|
+
return write.stagedOn !== targetTransaction;
|
|
1239
|
+
}
|
|
1240
|
+
if (write.deferSave && !write.saved && this.saveCommits)
|
|
1241
|
+
return ((!fromAbort || Boolean(targetTransaction) || Boolean(this.isCommitting)) &&
|
|
1242
|
+
!write.stagedOn &&
|
|
1243
|
+
!write.lockHandle?.released &&
|
|
1244
|
+
!write.lockHandle?.isExpired());
|
|
1245
|
+
if (!write.saved)
|
|
1246
|
+
return !fromAbort && Boolean(targetTransaction) && this.open === exports.TRANSACTION_STATE.OPEN && !write.stagedOn;
|
|
1247
|
+
const stagedOn = (write.stagedOn ?? this.activeCommitHandle ?? this.transaction);
|
|
1248
|
+
if (!stagedOn || stagedOn === targetTransaction || stagedOn.abortedByWrapper || this.isHandleCommitted(stagedOn))
|
|
1249
|
+
return false;
|
|
1250
|
+
if (excludeImmediate && stagedOn.immediateCommit)
|
|
1251
|
+
return false;
|
|
1252
|
+
if (!fromAbort &&
|
|
1253
|
+
Boolean(targetTransaction) &&
|
|
1254
|
+
this.open === exports.TRANSACTION_STATE.OPEN &&
|
|
1255
|
+
stagedOn === this.transaction)
|
|
1256
|
+
return true;
|
|
1257
|
+
if (hasSiblingInFlight &&
|
|
1258
|
+
(Boolean(targetTransaction) ||
|
|
1259
|
+
(!fromAbort && this.isActiveCommitHandle(stagedOn)) ||
|
|
1260
|
+
(fromAbort &&
|
|
1261
|
+
this.isActiveCommitHandle(stagedOn) &&
|
|
1262
|
+
((!excludeImmediate && Boolean(stagedOn.immediateCommit)) || Boolean(stagedOn.isRetry)))))
|
|
1263
|
+
return true;
|
|
1264
|
+
return Boolean(this.isHandleCommitInFlight(stagedOn) || hasDurableLog(stagedOn));
|
|
1265
|
+
}
|
|
1266
|
+
hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
|
|
1267
|
+
if (!this.stagesWriteOnSave)
|
|
1268
|
+
return false;
|
|
1269
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1270
|
+
if (this.isWriteOwnedByActiveCommit(this.writes[i], targetTransaction, hasSiblingInFlight, fromAbort, excludeImmediate))
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
hasSavedWrite() {
|
|
1276
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1277
|
+
if (this.writes[i]?.saved)
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
hasSavedWriteForHandle(transaction, isImmediateCommit) {
|
|
1283
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1284
|
+
const write = this.writes[i];
|
|
1285
|
+
if (!write?.saved)
|
|
1286
|
+
continue;
|
|
1287
|
+
if (isImmediateCommit ? write.stagedOn === transaction : !write.stagedOn || write.stagedOn === transaction) {
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
retainTransactionLockHolds(transaction) {
|
|
1294
|
+
if (!transaction || !this.hasLeaseProtectedWrite)
|
|
1295
|
+
return;
|
|
1296
|
+
const txn = transaction;
|
|
1297
|
+
if (txn.lockHolds || txn.holdBudgetSpent)
|
|
1298
|
+
return;
|
|
1299
|
+
const carrier = this.budgetCarrier(transaction);
|
|
1300
|
+
if (carrier.commitStartedAt == null)
|
|
1301
|
+
carrier.commitStartedAt = performance.now();
|
|
1302
|
+
const budget = this.commitConflictBudget();
|
|
1303
|
+
const holdDeadlineMono = Number.isFinite(budget) ? carrier.commitStartedAt + budget : undefined;
|
|
1304
|
+
let holds;
|
|
1305
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1306
|
+
const write = this.writes[i];
|
|
1307
|
+
if (!write?.saved || (write.stagedOn && write.stagedOn !== transaction))
|
|
1308
|
+
continue;
|
|
1309
|
+
const handle = write.lockHandle;
|
|
1310
|
+
if (!handle?.retainForCommit || (holds && holds.has(handle)))
|
|
1311
|
+
continue;
|
|
1312
|
+
handle.retainForCommit(holdDeadlineMono);
|
|
1313
|
+
(holds ??= new Set()).add(handle);
|
|
1314
|
+
}
|
|
1315
|
+
if (holds)
|
|
1316
|
+
txn.lockHolds = holds;
|
|
1317
|
+
}
|
|
1318
|
+
noteCommittedLockVersions(committedTransaction) {
|
|
995
1319
|
const recordLocks = this.recordLocks;
|
|
996
1320
|
if (!recordLocks)
|
|
997
1321
|
return;
|
|
1322
|
+
const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
|
|
1323
|
+
const filterByStagedOn = Boolean(committedTransaction || this.hasSiblingCommitInFlight(committedTransaction));
|
|
998
1324
|
for (const write of this.writes) {
|
|
999
|
-
if (write?.
|
|
1325
|
+
if (!write?.saved)
|
|
1326
|
+
continue;
|
|
1327
|
+
if (isImmediateCommit
|
|
1328
|
+
? write.stagedOn !== committedTransaction
|
|
1329
|
+
: filterByStagedOn && write.stagedOn && write.stagedOn !== committedTransaction)
|
|
1330
|
+
continue;
|
|
1331
|
+
if (write.appliedRecordVersion == null)
|
|
1000
1332
|
continue;
|
|
1001
1333
|
const handle = write.lockHandle ?? recordLocks.get(write.store)?.get(writeKeyId(write.key));
|
|
1002
1334
|
if (handle && !handle.released)
|
|
@@ -1005,9 +1337,32 @@ class DatabaseTransaction {
|
|
|
1005
1337
|
}
|
|
1006
1338
|
/**
|
|
1007
1339
|
* Discard the staged write set (committed or aborted); the per-key chain must go with it so a
|
|
1008
|
-
* reused transaction never bases a write on a previous batch's staged state.
|
|
1340
|
+
* reused transaction never bases a write on a previous batch's staged state. When concurrent
|
|
1341
|
+
* commits share one DatabaseTransaction, only the writes staged on `committedTransaction` are
|
|
1342
|
+
* removed while another native commit is still in flight.
|
|
1009
1343
|
*/
|
|
1010
|
-
clearWrites() {
|
|
1344
|
+
clearWrites(committedTransaction, fromAbort = false) {
|
|
1345
|
+
if (committedTransaction)
|
|
1346
|
+
this.releaseActiveCommitHandle(committedTransaction);
|
|
1347
|
+
const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
|
|
1348
|
+
const hasSiblingInFlight = this.hasSiblingCommitInFlight(committedTransaction);
|
|
1349
|
+
if (this.stagesWriteOnSave &&
|
|
1350
|
+
(isImmediateCommit ||
|
|
1351
|
+
((hasSiblingInFlight || !committedTransaction || this.commitsInFlight > 1 || this.saveCommits) &&
|
|
1352
|
+
this.hasWriteOwnedByActiveCommit(committedTransaction, hasSiblingInFlight, fromAbort)))) {
|
|
1353
|
+
for (const write of [...this.writes]) {
|
|
1354
|
+
if (!write || this.isWriteOwnedByActiveCommit(write, committedTransaction, hasSiblingInFlight, fromAbort))
|
|
1355
|
+
continue;
|
|
1356
|
+
this.detachWrite(write);
|
|
1357
|
+
}
|
|
1358
|
+
this.writes = this.writes.filter((write) => write);
|
|
1359
|
+
this.validated = this.writes.reduce((acc, w, idx) => (w?.saved ? idx + 1 : acc), 0);
|
|
1360
|
+
if (this.hasLeaseProtectedWrite && !this.writes.some((write) => write?.lockHandle)) {
|
|
1361
|
+
this.hasLeaseProtectedWrite = false;
|
|
1362
|
+
}
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
this.clearActiveCommitHandles();
|
|
1011
1366
|
// A deferred write's `stagedIn` must not outlive this transaction's ability to commit it: save() can
|
|
1012
1367
|
// fire after a commit or abort, and routing it back here would revive a write this transaction
|
|
1013
1368
|
// already rolled back — whose blobs abort() has reclaimed. Cleared here, save() resolves the
|
|
@@ -1019,6 +1374,7 @@ class DatabaseTransaction {
|
|
|
1019
1374
|
this.ownedWrites?.delete(write);
|
|
1020
1375
|
}
|
|
1021
1376
|
this.writes = [];
|
|
1377
|
+
this.validated = 0;
|
|
1022
1378
|
this.writesByKey = undefined;
|
|
1023
1379
|
// The commit-fence latch belongs to the discarded batch. A reused transaction that once staged
|
|
1024
1380
|
// a locked write would otherwise re-scan every write of every later unlocked batch. Assigned
|
|
@@ -1098,12 +1454,13 @@ class DatabaseTransaction {
|
|
|
1098
1454
|
* no record), and writes with no stored entry at all — a delete leaves a readable tombstone on any
|
|
1099
1455
|
* audited or delete-tracking table, so only a delete on a table with neither is entry-less. Such a
|
|
1100
1456
|
* `put(A); delete(B)` confirms on A's entry, whose version is this transaction's (every write in a
|
|
1101
|
-
* transaction is stamped with one version).
|
|
1457
|
+
* transaction is stamped with one version). `savedOnly` also skips a deferred write not yet saved,
|
|
1458
|
+
* which a commit leaves in place for a later one.
|
|
1102
1459
|
*/
|
|
1103
|
-
lastConfirmableEntry() {
|
|
1460
|
+
lastConfirmableEntry(savedOnly = false) {
|
|
1104
1461
|
for (let i = this.writes.length - 1; i >= 0; i--) {
|
|
1105
1462
|
const write = this.writes[i];
|
|
1106
|
-
if (!write || write.skipReplicationConfirmation)
|
|
1463
|
+
if (!write || write.skipReplicationConfirmation || (savedOnly && !write.saved))
|
|
1107
1464
|
continue;
|
|
1108
1465
|
const entry = write.store.getEntry(write.key);
|
|
1109
1466
|
if (entry)
|
|
@@ -1116,9 +1473,61 @@ class DatabaseTransaction {
|
|
|
1116
1473
|
* rejection in that window is an unhandled rejection (fatal under --unhandled-rejections=strict).
|
|
1117
1474
|
* The rejection still surfaces through commit()'s Promise.all.
|
|
1118
1475
|
*/
|
|
1119
|
-
stageCompletion(completion) {
|
|
1476
|
+
stageCompletion(completion, transaction) {
|
|
1120
1477
|
completion.then(undefined, () => { });
|
|
1478
|
+
const index = this.completions.length;
|
|
1121
1479
|
this.completions.push(completion);
|
|
1480
|
+
if (transaction &&
|
|
1481
|
+
(this.saveCommits ||
|
|
1482
|
+
Boolean(transaction.immediateCommit) ||
|
|
1483
|
+
this.hasActiveCommitHandle() ||
|
|
1484
|
+
Boolean(this.completionHandles))) {
|
|
1485
|
+
(this.completionHandles ??= [])[index] = transaction;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
takeCompletions(transaction) {
|
|
1489
|
+
if (this.completions.length === 0)
|
|
1490
|
+
return NO_COMPLETIONS;
|
|
1491
|
+
const handles = this.completionHandles;
|
|
1492
|
+
const isImmediate = Boolean(transaction?.immediateCommit);
|
|
1493
|
+
if (!handles) {
|
|
1494
|
+
if (isImmediate)
|
|
1495
|
+
return NO_COMPLETIONS;
|
|
1496
|
+
const completions = this.completions;
|
|
1497
|
+
this.completions = [];
|
|
1498
|
+
return completions;
|
|
1499
|
+
}
|
|
1500
|
+
let allMatch = true;
|
|
1501
|
+
for (let i = 0; i < this.completions.length; i++) {
|
|
1502
|
+
const handle = handles[i];
|
|
1503
|
+
if (!(handle ? handle === transaction : !isImmediate)) {
|
|
1504
|
+
allMatch = false;
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (allMatch) {
|
|
1509
|
+
const completions = this.completions;
|
|
1510
|
+
this.completions = [];
|
|
1511
|
+
this.completionHandles = undefined;
|
|
1512
|
+
return completions;
|
|
1513
|
+
}
|
|
1514
|
+
const taken = [];
|
|
1515
|
+
const remaining = [];
|
|
1516
|
+
const remainingHandles = [];
|
|
1517
|
+
for (let i = 0; i < this.completions.length; i++) {
|
|
1518
|
+
const completion = this.completions[i];
|
|
1519
|
+
const handle = handles[i];
|
|
1520
|
+
if (handle ? handle === transaction : !isImmediate) {
|
|
1521
|
+
taken.push(completion);
|
|
1522
|
+
}
|
|
1523
|
+
else {
|
|
1524
|
+
remaining.push(completion);
|
|
1525
|
+
remainingHandles.push(handle);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
this.completions = remaining;
|
|
1529
|
+
this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
|
|
1530
|
+
return taken;
|
|
1122
1531
|
}
|
|
1123
1532
|
/**
|
|
1124
1533
|
* Discard staged completions that no commit() will ever aggregate (abort path). Their rejections
|
|
@@ -1126,12 +1535,41 @@ class DatabaseTransaction {
|
|
|
1126
1535
|
* Clearing them also keeps a reused transaction's next commit() from rejecting with the previous
|
|
1127
1536
|
* batch's error.
|
|
1128
1537
|
*/
|
|
1129
|
-
drainCompletions() {
|
|
1538
|
+
drainCompletions(targetTransaction, preserveActiveWrites = false) {
|
|
1130
1539
|
if (this.completions.length === 0)
|
|
1131
1540
|
return;
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1541
|
+
const handles = this.completionHandles;
|
|
1542
|
+
const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
|
|
1543
|
+
let drained;
|
|
1544
|
+
if (!preserveActiveWrites) {
|
|
1545
|
+
drained = this.completions;
|
|
1546
|
+
this.completions = [];
|
|
1547
|
+
this.completionHandles = undefined;
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
drained = [];
|
|
1551
|
+
const remaining = [];
|
|
1552
|
+
const remainingHandles = [];
|
|
1553
|
+
for (let i = 0; i < this.completions.length; i++) {
|
|
1554
|
+
const completion = this.completions[i];
|
|
1555
|
+
const handle = handles?.[i];
|
|
1556
|
+
const keep = isImmediateTarget
|
|
1557
|
+
? handle !== targetTransaction
|
|
1558
|
+
: Boolean(handle) &&
|
|
1559
|
+
handle !== targetTransaction &&
|
|
1560
|
+
(this.isActiveCommitHandle(handle) || hasDurableLog(handle));
|
|
1561
|
+
if (keep) {
|
|
1562
|
+
remaining.push(completion);
|
|
1563
|
+
remainingHandles.push(handle);
|
|
1564
|
+
}
|
|
1565
|
+
else {
|
|
1566
|
+
drained.push(completion);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
this.completions = remaining;
|
|
1570
|
+
this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
|
|
1571
|
+
}
|
|
1572
|
+
for (const completion of drained)
|
|
1135
1573
|
completion.then(undefined, (error) => harperLogger.warn?.('A staged transaction completion failed after the transaction was aborted', error));
|
|
1136
1574
|
}
|
|
1137
1575
|
addWrite(operation) {
|
|
@@ -1205,7 +1643,7 @@ class DatabaseTransaction {
|
|
|
1205
1643
|
// (the same rule the pre-submit fence applies below). Without the distinction,
|
|
1206
1644
|
// `lock(); save(); unlock(); commit()` succeeded normally and threw 409 only when a conflict
|
|
1207
1645
|
// retry or an open read iterator forced the replay to re-save it.
|
|
1208
|
-
if (operation.saved ? lockHandle?.isLeaseExpired() : lockHandle?.isExpired()) {
|
|
1646
|
+
if (operation.saved ? lockHandle?.isLeaseExpired() && !hasDurableLog(transaction) : lockHandle?.isExpired()) {
|
|
1209
1647
|
// Through `detachWrite`, which also repairs the per-key chain: dropping it from `writes` alone
|
|
1210
1648
|
// leaves `writesByKey` pointing at a write that was refused, so a caller that catches this 409
|
|
1211
1649
|
// and stages the same key again would take the rejected operation as its merge basis.
|
|
@@ -1229,7 +1667,10 @@ class DatabaseTransaction {
|
|
|
1229
1667
|
operation.lockStamp = lockHandle.holderVersionCandidate();
|
|
1230
1668
|
}
|
|
1231
1669
|
}
|
|
1232
|
-
|
|
1670
|
+
const concurrentClosedWrite = Boolean(transaction?.immediateCommit) ||
|
|
1671
|
+
(!transaction && this.open !== exports.TRANSACTION_STATE.OPEN && !this.saveCommits);
|
|
1672
|
+
let txnTime = operation.lockStamp ?? this.getHandleStagedTxnTime(transaction) ?? (concurrentClosedWrite ? 0 : this.timestamp);
|
|
1673
|
+
const mintedTimestamp = !txnTime;
|
|
1233
1674
|
// Only an OPEN transaction accepts new staged writes. After commit, this.transaction may still
|
|
1234
1675
|
// be retained for outstanding read iterators; staging into it would silently discard the write
|
|
1235
1676
|
// when doneReadTxn() aborts the handle, so such writes commit immediately on a fresh
|
|
@@ -1260,20 +1701,43 @@ class DatabaseTransaction {
|
|
|
1260
1701
|
else {
|
|
1261
1702
|
// if it is closed, we have to immediately commit, using our immediate transaction
|
|
1262
1703
|
immediateCommit = true;
|
|
1704
|
+
if (!this.saveCommits)
|
|
1705
|
+
transaction.immediateCommit = true;
|
|
1263
1706
|
}
|
|
1264
1707
|
if (txnTime) {
|
|
1265
1708
|
transaction.setTimestamp(txnTime);
|
|
1266
1709
|
}
|
|
1267
1710
|
}
|
|
1711
|
+
if (this.activeCommitHandle && this.activeCommitHandle !== transaction) {
|
|
1712
|
+
this.backfillActiveHandleWrites(this.activeCommitHandle);
|
|
1713
|
+
}
|
|
1714
|
+
else if (!this.activeCommitHandle && this.transaction && this.transaction !== transaction) {
|
|
1715
|
+
this.backfillActiveHandleWrites(this.transaction);
|
|
1716
|
+
}
|
|
1717
|
+
const shouldStampStagedOn = this.saveCommits ||
|
|
1718
|
+
immediateCommit ||
|
|
1719
|
+
Boolean(transaction?.immediateCommit) ||
|
|
1720
|
+
operation.stagedOn !== undefined ||
|
|
1721
|
+
(this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0) ||
|
|
1722
|
+
(this.activeCommitHandle !== undefined && this.activeCommitHandle !== transaction) ||
|
|
1723
|
+
(this.transaction != null && this.transaction !== transaction);
|
|
1268
1724
|
if (this.isReplay) {
|
|
1269
1725
|
// Replayed writes came FROM the transaction log; never re-append them —
|
|
1270
1726
|
// replay iterates that same log, so re-appending prevents convergence
|
|
1271
1727
|
// (boot hangs replaying its own output). Conflict retries stamp isRetry
|
|
1272
1728
|
// at the retry sites in commit(); this is the replay-path equivalent.
|
|
1273
1729
|
transaction.isRetry = true;
|
|
1730
|
+
transaction.retries = this.retries;
|
|
1274
1731
|
}
|
|
1275
|
-
if (!txnTime)
|
|
1276
|
-
txnTime =
|
|
1732
|
+
if (!txnTime) {
|
|
1733
|
+
txnTime = transaction.getTimestamp();
|
|
1734
|
+
if (!concurrentClosedWrite)
|
|
1735
|
+
this.timestamp = txnTime;
|
|
1736
|
+
}
|
|
1737
|
+
if (!operation.lockStamp)
|
|
1738
|
+
this.setHandleStagedTxnTime(transaction, txnTime);
|
|
1739
|
+
if (this.saveCommits && mintedTimestamp)
|
|
1740
|
+
this.timestamp = 0;
|
|
1277
1741
|
if (!operation.saved && operation.pendingPriorWrite) {
|
|
1278
1742
|
const pendingWrites = [];
|
|
1279
1743
|
for (let pending = operation.pendingPriorWrite; pending;) {
|
|
@@ -1301,50 +1765,78 @@ class DatabaseTransaction {
|
|
|
1301
1765
|
// An entry read uncached through this same pinned-snapshot handle is already that base, until a retry resets
|
|
1302
1766
|
// the snapshot or an earlier staged write to the key changes what a read returns; the resource withholds the
|
|
1303
1767
|
// handle once its entry was replaced or evicted, and locked, snapshot-free and replayed writes always reload.
|
|
1768
|
+
const txnRetries = transaction?.retries ?? 0;
|
|
1304
1769
|
const reusesBaseRead = operation.baseReadTxn === transaction &&
|
|
1305
|
-
|
|
1770
|
+
txnRetries === 0 &&
|
|
1306
1771
|
!reloadEntry &&
|
|
1307
1772
|
!operation.priorWrite &&
|
|
1308
1773
|
!operation.lockHandle &&
|
|
1309
1774
|
!this.snapshotFree &&
|
|
1310
1775
|
!transaction.snapshotDisabled &&
|
|
1311
1776
|
!this.isReplay;
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
if (
|
|
1777
|
+
const wasSaved = Boolean(operation.saved);
|
|
1778
|
+
let completion;
|
|
1779
|
+
try {
|
|
1780
|
+
if (!reusesBaseRead && (reloadEntry || operation.entry === undefined || reloadsCommitBase)) {
|
|
1781
|
+
const uncachedRead = !!operation.reloadCommitBase || reloadEntry || this.isReplay;
|
|
1782
|
+
operation.entry = operation.store.getEntry(operation.key, { transaction, uncachedRead });
|
|
1783
|
+
}
|
|
1784
|
+
if (!operation.saved) {
|
|
1785
|
+
// immediately execute in this transaction
|
|
1786
|
+
const validated = validateWrite(operation, writeVersion, this);
|
|
1787
|
+
if (validated === false) {
|
|
1788
|
+
operation.saved = true;
|
|
1789
|
+
if (shouldStampStagedOn)
|
|
1790
|
+
operation.stagedOn = transaction;
|
|
1791
|
+
operation.commit = () => { }; // noop if we try again
|
|
1792
|
+
closeWriteInstance(operation);
|
|
1793
|
+
if (immediateCommit) {
|
|
1794
|
+
this.detachWrite(operation);
|
|
1795
|
+
if (!this.activeCommitHandle)
|
|
1796
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1797
|
+
abortNativeTransaction(transaction, 'aborting an immediate transaction with nothing to write', operation.store, this.startedFrom);
|
|
1798
|
+
}
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1320
1801
|
operation.saved = true;
|
|
1321
|
-
|
|
1802
|
+
let result = operation.before?.();
|
|
1803
|
+
if (result?.then)
|
|
1804
|
+
this.stageCompletion(result, transaction);
|
|
1805
|
+
result = operation.beforeIntermediate?.();
|
|
1806
|
+
if (result?.then)
|
|
1807
|
+
this.stageCompletion(result, transaction);
|
|
1808
|
+
}
|
|
1809
|
+
if (shouldStampStagedOn)
|
|
1810
|
+
operation.stagedOn = transaction;
|
|
1811
|
+
if (lockHandle || this.recordLocks)
|
|
1812
|
+
operation.trackRecordVersion = true;
|
|
1813
|
+
if (operation.trackRecordVersion)
|
|
1814
|
+
operation.recordVersionApplied = false;
|
|
1815
|
+
operation.committedOn = transaction;
|
|
1816
|
+
operation.committedRound = txnRetries;
|
|
1817
|
+
try {
|
|
1818
|
+
completion = operation.commit(writeVersion, operation.entry, (wasSaved || this.isReplay) && txnRetries > 0, transaction, this);
|
|
1819
|
+
}
|
|
1820
|
+
finally {
|
|
1322
1821
|
closeWriteInstance(operation);
|
|
1323
|
-
return;
|
|
1324
1822
|
}
|
|
1325
|
-
operation.saved = true;
|
|
1326
|
-
let result = operation.before?.();
|
|
1327
|
-
if (result?.then)
|
|
1328
|
-
this.stageCompletion(result);
|
|
1329
|
-
result = operation.beforeIntermediate?.();
|
|
1330
|
-
if (result?.then)
|
|
1331
|
-
this.stageCompletion(result);
|
|
1332
|
-
}
|
|
1333
|
-
if (lockHandle || this.recordLocks)
|
|
1334
|
-
operation.trackRecordVersion = true;
|
|
1335
|
-
if (operation.trackRecordVersion)
|
|
1336
|
-
operation.recordVersionApplied = false;
|
|
1337
|
-
let completion;
|
|
1338
|
-
try {
|
|
1339
|
-
completion = operation.commit(writeVersion, operation.entry, this.retries > 0, transaction);
|
|
1340
1823
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1824
|
+
catch (error) {
|
|
1825
|
+
// A handle created for an immediate commit is owned by nobody else: a throw before its commit
|
|
1826
|
+
// is submitted must release it rather than leak it.
|
|
1827
|
+
if (immediateCommit) {
|
|
1828
|
+
this.drainCompletions(transaction, true);
|
|
1829
|
+
this.detachWrite(operation);
|
|
1830
|
+
if (!this.activeCommitHandle)
|
|
1831
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1832
|
+
abortNativeTransaction(transaction, 'aborting an immediate transaction whose write threw', operation.store, this.startedFrom);
|
|
1833
|
+
}
|
|
1834
|
+
throw error;
|
|
1343
1835
|
}
|
|
1344
1836
|
if (operation.trackRecordVersion)
|
|
1345
1837
|
operation.appliedRecordVersion = operation.recordVersionApplied ? writeVersion : undefined;
|
|
1346
1838
|
if (typeof completion?.then === 'function')
|
|
1347
|
-
this.stageCompletion(completion);
|
|
1839
|
+
this.stageCompletion(completion, transaction);
|
|
1348
1840
|
// Sticky record that THIS write staged with its audit entry appended (log entries batch on the
|
|
1349
1841
|
// native transaction and are durably written by its commit attempt — even a failed one — so
|
|
1350
1842
|
// they survive the abort-after-failed-commit of the retry paths). isRetry stagings
|
|
@@ -1353,6 +1845,7 @@ class DatabaseTransaction {
|
|
|
1353
1845
|
// retries where a recommit round self-skips before a fresh-transaction replay.
|
|
1354
1846
|
if (!operation.skipped && !transaction.isRetry) {
|
|
1355
1847
|
operation.appendedAuditEntry = true;
|
|
1848
|
+
operation.appendedTo = transaction;
|
|
1356
1849
|
}
|
|
1357
1850
|
if (immediateCommit) {
|
|
1358
1851
|
// immediately commit if the harper transaction is closed
|
|
@@ -1549,6 +2042,10 @@ class DatabaseTransaction {
|
|
|
1549
2042
|
endCommitAttempt() {
|
|
1550
2043
|
this.commitsInFlight--;
|
|
1551
2044
|
if (!this.commitsInFlight) {
|
|
2045
|
+
this.clearActiveCommitHandles();
|
|
2046
|
+
this.commitStartedAt = undefined;
|
|
2047
|
+
if (this.commitOverdueLogged)
|
|
2048
|
+
this.commitOverdueLogged = undefined;
|
|
1552
2049
|
this.nativeCommitSubmitted = false;
|
|
1553
2050
|
this.nativeCommitAttemptEnded();
|
|
1554
2051
|
}
|
|
@@ -1568,7 +2065,14 @@ class DatabaseTransaction {
|
|
|
1568
2065
|
root.submittedCommitSupervised = false;
|
|
1569
2066
|
root.deferredPoisonDeadline = undefined;
|
|
1570
2067
|
root.unsubmittedCommitDeadline = undefined;
|
|
2068
|
+
root.loggedRetriesSeen = undefined;
|
|
2069
|
+
root.loggedRetriesChangedAt = undefined;
|
|
2070
|
+
root.loggedRetriesTick = undefined;
|
|
2071
|
+
root.loggedRetryAdvancing = undefined;
|
|
1571
2072
|
root.stalledCommitLogged = false;
|
|
2073
|
+
root.commitStartedAt = undefined;
|
|
2074
|
+
if (root.commitOverdueLogged)
|
|
2075
|
+
root.commitOverdueLogged = undefined;
|
|
1572
2076
|
const submittedLink = root.submittedLink;
|
|
1573
2077
|
root.submittedLink = undefined;
|
|
1574
2078
|
const submittedLinks = root.submittedLinks;
|
|
@@ -1643,35 +2147,57 @@ class DatabaseTransaction {
|
|
|
1643
2147
|
// reused across retries — the native layer resets it in place (fresh snapshot) on IsBusy/TryAgain —
|
|
1644
2148
|
// but reassigned to a fresh replay transaction when outstanding read iterators retain this.transaction
|
|
1645
2149
|
let transaction = options.transaction ?? this.transaction;
|
|
2150
|
+
const retryRound = Boolean(options.transaction?.isRetry);
|
|
2151
|
+
const isImmediateCommit = Boolean(transaction?.immediateCommit);
|
|
1646
2152
|
try {
|
|
2153
|
+
if (retryRound && this.activeCommitHandle && this.activeCommitHandle !== transaction) {
|
|
2154
|
+
this.backfillActiveHandleWrites(this.activeCommitHandle);
|
|
2155
|
+
}
|
|
1647
2156
|
for (let i = 0; i < this.writes.length; i++) {
|
|
1648
2157
|
let operation = this.writes[i];
|
|
1649
|
-
if (!operation ||
|
|
2158
|
+
if (!operation ||
|
|
2159
|
+
(isImmediateCommit
|
|
2160
|
+
? !retryRound || !operation.saved || operation.stagedOn !== transaction
|
|
2161
|
+
: retryRound
|
|
2162
|
+
? !operation.saved || (Boolean(operation.stagedOn) && operation.stagedOn !== transaction)
|
|
2163
|
+
: Boolean(operation.saved) || (this.saveCommits && Boolean(operation.deferSave))))
|
|
1650
2164
|
continue;
|
|
1651
|
-
this.save(operation, transaction, i < this.validated, options);
|
|
2165
|
+
this.save(operation, transaction, Boolean(operation.saved) || i < this.validated, options);
|
|
2166
|
+
if (!transaction)
|
|
2167
|
+
transaction = this.transaction;
|
|
1652
2168
|
}
|
|
2169
|
+
if (transaction && (this.writes.length > 0 || hasDurableLog(transaction)))
|
|
2170
|
+
this.trackActiveCommitHandle(transaction);
|
|
1653
2171
|
}
|
|
1654
2172
|
catch (error) {
|
|
2173
|
+
if (!transaction)
|
|
2174
|
+
transaction = this.transaction ?? undefined;
|
|
1655
2175
|
// abort() releases only this.transaction; a retry round's handle was detached before its
|
|
1656
2176
|
// first submission and would otherwise hold its write intents until GC.
|
|
1657
|
-
if (transaction !== this.transaction)
|
|
1658
|
-
abortNativeTransaction(transaction, 'aborting a retry transaction whose re-save threw');
|
|
1659
|
-
this.abort();
|
|
2177
|
+
if (transaction && transaction !== this.transaction)
|
|
2178
|
+
abortNativeTransaction(transaction, 'aborting a retry transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2179
|
+
this.abort(false, true, transaction);
|
|
1660
2180
|
throw error;
|
|
1661
2181
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2182
|
+
if (!isImmediateCommit) {
|
|
2183
|
+
this.validated = this.saveCommits
|
|
2184
|
+
? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
|
|
2185
|
+
: this.writes.length;
|
|
2186
|
+
}
|
|
2187
|
+
const completions = this.takeCompletions(transaction);
|
|
1666
2188
|
const stagedWrites = this.writes.length;
|
|
1667
|
-
|
|
2189
|
+
let inCommitPhase = false;
|
|
2190
|
+
if (completions.length > 0 && !isImmediateCommit) {
|
|
2191
|
+
inCommitPhase = true;
|
|
1668
2192
|
this.setCommitPhase(true);
|
|
1669
2193
|
}
|
|
1670
2194
|
let commitResult;
|
|
1671
2195
|
try {
|
|
1672
2196
|
commitResult = (0, when_ts_1.when)(completions.length > 0 ? Promise.all(completions) : null, () => {
|
|
1673
|
-
if (
|
|
2197
|
+
if (inCommitPhase) {
|
|
2198
|
+
inCommitPhase = false;
|
|
1674
2199
|
this.setCommitPhase(false);
|
|
2200
|
+
}
|
|
1675
2201
|
// The transaction can be aborted underneath us while we are parked in the await above — by the
|
|
1676
2202
|
// monitor once the commit phase outlives its grace, or through the multi-store poison chain.
|
|
1677
2203
|
// abort() cleared the write set and released the handle, so resuming would commit nothing and
|
|
@@ -1685,7 +2211,11 @@ class DatabaseTransaction {
|
|
|
1685
2211
|
}
|
|
1686
2212
|
if (stagedWrites > 0 && this.writes.length === 0 && this.open === exports.TRANSACTION_STATE.CLOSED)
|
|
1687
2213
|
throw new hdbError_ts_1.ServerError('Transaction was aborted while its commit was waiting on pre-commit work', 500);
|
|
1688
|
-
if (
|
|
2214
|
+
if (!retryRound &&
|
|
2215
|
+
!isImmediateCommit &&
|
|
2216
|
+
(this.saveCommits
|
|
2217
|
+
? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
|
|
2218
|
+
: this.writes.length) > this.validated) {
|
|
1689
2219
|
// check just in case we got any more transactions while we were waiting, if so just recursively continue to finish the additional writes now
|
|
1690
2220
|
return this.commit({ ...options, continuation: true });
|
|
1691
2221
|
}
|
|
@@ -1697,9 +2227,10 @@ class DatabaseTransaction {
|
|
|
1697
2227
|
// and replay branches below deliberately commit a handle other than this.transaction.
|
|
1698
2228
|
if (!transaction)
|
|
1699
2229
|
transaction = this.transaction;
|
|
1700
|
-
if (!options.transaction && this.
|
|
2230
|
+
if (!options.transaction && this.hasSavedWrite())
|
|
1701
2231
|
this.renewChainForNativeCommit();
|
|
1702
|
-
|
|
2232
|
+
if (!isImmediateCommit)
|
|
2233
|
+
this.open = exports.TRANSACTION_STATE.CLOSED;
|
|
1703
2234
|
// RocksTransaction.commit() resolves with RETRY_NOW_VALUE (a number) under
|
|
1704
2235
|
// coordinatedRetry, or void on a normal commit/abort.
|
|
1705
2236
|
let commitResolution;
|
|
@@ -1707,11 +2238,12 @@ class DatabaseTransaction {
|
|
|
1707
2238
|
// recursions, immediate-commit re-entries, and a second top-level commit() (wrapper
|
|
1708
2239
|
// commit after an explicit in-handler commit) must not steal a reference owned by an
|
|
1709
2240
|
// outstanding iterator (doneReadTxn() would then never release the native handle).
|
|
1710
|
-
if (!this.baseReadRefConsumed) {
|
|
2241
|
+
if (!isImmediateCommit && !this.baseReadRefConsumed) {
|
|
1711
2242
|
this.baseReadRefConsumed = true;
|
|
1712
2243
|
this.readTxnsUsed--;
|
|
1713
2244
|
}
|
|
1714
|
-
|
|
2245
|
+
let hadLeaseProtectedWrite = false;
|
|
2246
|
+
if (!isImmediateCommit && this.readTxnsUsed > 0) {
|
|
1715
2247
|
// Outstanding iterators still stream through this.transaction — their native iterators
|
|
1716
2248
|
// live inside it (GetIterator wraps its write batch + snapshot), so committing or
|
|
1717
2249
|
// aborting the handle now would invalidate them mid-stream. Leave it open for the
|
|
@@ -1723,7 +2255,7 @@ class DatabaseTransaction {
|
|
|
1723
2255
|
// kept the last iterator from finishing cleanly — a hung stream, the long-transaction
|
|
1724
2256
|
// monitor's timeout abort — dropped writes the caller had already been told committed.
|
|
1725
2257
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
1726
|
-
if (this.
|
|
2258
|
+
if (this.hasSavedWrite() || hasDurableLog(transaction)) {
|
|
1727
2259
|
if (!options.transaction) {
|
|
1728
2260
|
if (!replayedWritesWarned) {
|
|
1729
2261
|
replayedWritesWarned = true;
|
|
@@ -1741,24 +2273,48 @@ class DatabaseTransaction {
|
|
|
1741
2273
|
const replayTransaction = new rocksdb_js_1.Transaction((this.writes[0].store.store ?? this.db.store), { coordinatedRetry: true });
|
|
1742
2274
|
if (this.timestamp)
|
|
1743
2275
|
replayTransaction.setTimestamp(this.timestamp);
|
|
1744
|
-
|
|
2276
|
+
const retries = (transaction?.retries ?? this.retries) + 1;
|
|
2277
|
+
replayTransaction.retries = retries;
|
|
2278
|
+
this.retries = retries; // a replay round: commit handlers re-base on the reloaded entries
|
|
2279
|
+
const prevStagedTxnTime = this.getHandleStagedTxnTime(transaction);
|
|
2280
|
+
if (transaction)
|
|
2281
|
+
this.releaseActiveCommitHandle(transaction);
|
|
2282
|
+
this.trackActiveCommitHandle(replayTransaction);
|
|
2283
|
+
if (prevStagedTxnTime)
|
|
2284
|
+
this.setHandleStagedTxnTime(replayTransaction, prevStagedTxnTime);
|
|
2285
|
+
const abandoned = transaction;
|
|
2286
|
+
const abandonedLogged = hasDurableLog(abandoned);
|
|
2287
|
+
transaction = replayTransaction;
|
|
1745
2288
|
try {
|
|
1746
2289
|
for (const operation of this.writes) {
|
|
2290
|
+
if (!operation?.saved)
|
|
2291
|
+
continue;
|
|
2292
|
+
// A non-durable abandoned handle left no readable audit entry; a durable one may
|
|
2293
|
+
// have published its log and must retain the retry shield.
|
|
2294
|
+
if (operation.appendedTo === abandoned && !abandonedLogged) {
|
|
2295
|
+
operation.appendedAuditEntry = false;
|
|
2296
|
+
operation.appendedTo = undefined;
|
|
2297
|
+
}
|
|
1747
2298
|
this.save(operation, replayTransaction, true, options);
|
|
1748
2299
|
}
|
|
1749
2300
|
}
|
|
1750
2301
|
catch (error) {
|
|
1751
|
-
abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw');
|
|
1752
|
-
this.
|
|
2302
|
+
abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2303
|
+
this.releaseActiveCommitHandle(replayTransaction);
|
|
2304
|
+
this.abort(false, true, this.hasSiblingCommitInFlight(replayTransaction) ? replayTransaction : undefined);
|
|
1753
2305
|
throw error;
|
|
1754
2306
|
}
|
|
1755
|
-
transaction = replayTransaction;
|
|
1756
2307
|
}
|
|
1757
2308
|
// with options.transaction set this is a retry round — the save loop above already
|
|
1758
2309
|
// re-staged the writes into it
|
|
1759
2310
|
this.assertDatabaseCommitAllowed(transaction);
|
|
1760
2311
|
this.markNativeCommitSubmitted();
|
|
1761
2312
|
commitResolution = transaction.commit();
|
|
2313
|
+
this.setHandleCommitInFlight(transaction, true);
|
|
2314
|
+
if (this.hasLeaseProtectedWrite) {
|
|
2315
|
+
hadLeaseProtectedWrite = true;
|
|
2316
|
+
this.retainTransactionLockHolds(transaction);
|
|
2317
|
+
}
|
|
1762
2318
|
recordCommitLatency(commitResolution, performance.now());
|
|
1763
2319
|
// Write-queue-depth accounting for this replay commit happens uniformly below, via
|
|
1764
2320
|
// trackOutstandingCommit(commitResolution) — see that function's comment. Omitting
|
|
@@ -1779,10 +2335,11 @@ class DatabaseTransaction {
|
|
|
1779
2335
|
}
|
|
1780
2336
|
else {
|
|
1781
2337
|
// no more reads need to be performed, just commit/abort based if there are any writes
|
|
1782
|
-
|
|
2338
|
+
if (!isImmediateCommit)
|
|
2339
|
+
this.detachOwnedTransaction(); // any further operations operate immediately
|
|
1783
2340
|
if (transaction) {
|
|
1784
2341
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
1785
|
-
if (this.
|
|
2342
|
+
if (this.hasSavedWriteForHandle(transaction, isImmediateCommit) || hasDurableLog(transaction)) {
|
|
1786
2343
|
// Commit retries can construct fresh ranges on this live handle after read ownership ends.
|
|
1787
2344
|
readTransactionOwners.delete(transaction);
|
|
1788
2345
|
// Re-fence before submitting: the loop above skips operations already marked saved, so
|
|
@@ -1790,25 +2347,30 @@ class DatabaseTransaction {
|
|
|
1790
2347
|
// retry/replay path re-saves every operation and is fenced there instead. Lease expiry
|
|
1791
2348
|
// only: an unlock() inside the lease leaves the staged write valid, an elapsed lease
|
|
1792
2349
|
// does not, whether or not the caller also unlocked.
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
this
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2350
|
+
if (this.hasLeaseProtectedWrite && !hasDurableLog(transaction)) {
|
|
2351
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
2352
|
+
const write = this.writes[i];
|
|
2353
|
+
if (!write?.saved ||
|
|
2354
|
+
(isImmediateCommit
|
|
2355
|
+
? write.stagedOn !== transaction
|
|
2356
|
+
: Boolean(write.stagedOn) && write.stagedOn !== transaction))
|
|
2357
|
+
continue;
|
|
2358
|
+
const lapsed = write.lockHandle;
|
|
2359
|
+
if (!lapsed?.isLeaseExpired())
|
|
2360
|
+
continue;
|
|
2361
|
+
abortNativeTransaction(transaction, 'aborting a transaction whose record lock lapsed before submission', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2362
|
+
// Every other terminal exit from commit() runs the logical cleanup too. Throwing
|
|
2363
|
+
// straight out would strand the OTHER locks this transaction holds, its staged
|
|
2364
|
+
// blobs, and the context's back-reference to a CLOSED transaction — and
|
|
2365
|
+
// transaction()'s onComplete has no rejection path to run it later.
|
|
2366
|
+
try {
|
|
2367
|
+
this.abort(false, true, transaction);
|
|
2368
|
+
}
|
|
2369
|
+
catch (error) {
|
|
2370
|
+
harperLogger.debug?.('cleaning up a transaction whose record lock lapsed', error);
|
|
2371
|
+
}
|
|
2372
|
+
throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
|
|
1810
2373
|
}
|
|
1811
|
-
throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
|
|
1812
2374
|
}
|
|
1813
2375
|
// The transaction was created with coordinatedRetry:true (see
|
|
1814
2376
|
// getReadTxn), so commit() can resolve to RETRY_NOW_VALUE. That
|
|
@@ -1817,6 +2379,11 @@ class DatabaseTransaction {
|
|
|
1817
2379
|
this.assertDatabaseCommitAllowed(transaction);
|
|
1818
2380
|
this.markNativeCommitSubmitted();
|
|
1819
2381
|
commitResolution = transaction.commit();
|
|
2382
|
+
this.setHandleCommitInFlight(transaction, true);
|
|
2383
|
+
if (this.hasLeaseProtectedWrite) {
|
|
2384
|
+
hadLeaseProtectedWrite = true;
|
|
2385
|
+
this.retainTransactionLockHolds(transaction);
|
|
2386
|
+
}
|
|
1820
2387
|
// Record how long this commit stays outstanding (submit → settle) as a distribution
|
|
1821
2388
|
// metric. This is the same clock the overload check uses (trackOutstandingCommit
|
|
1822
2389
|
// stamps each attempt at submit), so a rising p99/p999 is the leading indicator for the
|
|
@@ -1836,7 +2403,10 @@ class DatabaseTransaction {
|
|
|
1836
2403
|
try {
|
|
1837
2404
|
commitResolution = transaction.abort();
|
|
1838
2405
|
}
|
|
1839
|
-
catch {
|
|
2406
|
+
catch (abortError) {
|
|
2407
|
+
if (abortError?.code === 'ERR_TRANSACTION_ABANDONED') {
|
|
2408
|
+
harperLogger.error?.(`Aborted a transaction with a durable log entry (empty write set), ${describeCommitIdentity(this.db, this.startedFrom, transaction)}`, abortError);
|
|
2409
|
+
}
|
|
1840
2410
|
// The transaction has uncommitted writes that were already cleared from
|
|
1841
2411
|
// this.writes by a concurrent immediate-commit path (e.g. writes made with
|
|
1842
2412
|
// an explicitly-reused closed transaction). Those writes are handled by the
|
|
@@ -1849,38 +2419,53 @@ class DatabaseTransaction {
|
|
|
1849
2419
|
// Read the table off the write itself, not this.db, which is whichever table first
|
|
1850
2420
|
// claimed this per-database transaction in txnForContext and so can name the wrong
|
|
1851
2421
|
// table when a transaction spans more than one table in the same database.
|
|
1852
|
-
trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
|
|
2422
|
+
trackOutstandingCommit(commitResolution, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
|
|
1853
2423
|
// Every retry round and every chained store re-enters here and must inherit the chain
|
|
1854
|
-
// root's clock rather than restart it
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2424
|
+
// root's clock rather than restart it (or the per-handle clock on an unchained
|
|
2425
|
+
// ImmediateTransaction), so only the first submission stamps.
|
|
2426
|
+
const carrier = this.budgetCarrier(transaction);
|
|
2427
|
+
if (carrier.commitStartedAt == null)
|
|
2428
|
+
carrier.commitStartedAt = performance.now();
|
|
1858
2429
|
const completions = [];
|
|
1859
2430
|
const commitOutcome = commitResolution.then((commitResult) => {
|
|
2431
|
+
const txnWithRetry = transaction;
|
|
1860
2432
|
if (commitResult === RETRY_NOW_VALUE) {
|
|
1861
|
-
this.
|
|
1862
|
-
|
|
2433
|
+
this.setHandleCommitInFlight(transaction, false);
|
|
2434
|
+
const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
|
|
2435
|
+
if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
|
|
2436
|
+
this.retries = retries;
|
|
2437
|
+
harperLogger.debug?.('coordinated retry', transaction.id, retries);
|
|
1863
2438
|
// Mark this specific native transaction as a retry so RocksTransactionLogStore
|
|
1864
2439
|
// skips re-writing its already-staged txn-log entries (#2).
|
|
1865
|
-
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2440
|
+
txnWithRetry.isRetry = true;
|
|
2441
|
+
const logged = hasDurableLog(transaction);
|
|
2442
|
+
const pastBudget = this.elapsedPastCommitBudget(transaction);
|
|
2443
|
+
if (pastBudget) {
|
|
2444
|
+
if (logged)
|
|
2445
|
+
this.noteOverdueLoggedCommit(transaction, pastBudget);
|
|
2446
|
+
else
|
|
2447
|
+
this.abandonCommitAfterDeadline(transaction, pastBudget);
|
|
2448
|
+
}
|
|
2449
|
+
if (retries > MAX_RETRIES && !this.sourceApply && !logged) {
|
|
2450
|
+
// giving up: poison and abort the whole linked chain so no link leaks its native
|
|
2451
|
+
// handle / read snapshot — or any unpublished transaction-log position — until GC.
|
|
2452
|
+
this.abortChainAfterRetries(transaction);
|
|
2453
|
+
throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} coordinated retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
|
|
2454
|
+
}
|
|
2455
|
+
if ((this.sourceApply || logged) && retries > 0 && retries % MAX_RETRIES === 0) {
|
|
2456
|
+
harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} coordinated retries; continuing to retry`);
|
|
2457
|
+
}
|
|
2458
|
+
// Randomized so two uncapped logged commits crossing on the same keys stop waking in lockstep.
|
|
2459
|
+
if (logged && retries > 2) {
|
|
2460
|
+
const backoff = (0, promises_1.setTimeout)(Math.ceil(Math.min(retries * 2, MAX_RETRY_DELAY_MS) * Math.random()));
|
|
2461
|
+
trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
|
|
2462
|
+
return backoff.then(() => this.commit({ ...options, transaction }));
|
|
1881
2463
|
}
|
|
1882
2464
|
return this.commit({ ...options, transaction });
|
|
1883
2465
|
}
|
|
2466
|
+
const committedTransaction = transaction;
|
|
2467
|
+
const committedImmediate = Boolean(committedTransaction?.immediateCommit);
|
|
2468
|
+
this.setHandleCommitted(transaction);
|
|
1884
2469
|
// onCommit may be async (e.g. RocksTransactionLogStore emits 'aftercommit'). Surface a
|
|
1885
2470
|
// rejection — or a synchronous throw — via logging rather than failing the commit, since
|
|
1886
2471
|
// the write is already durable.
|
|
@@ -1892,7 +2477,14 @@ class DatabaseTransaction {
|
|
|
1892
2477
|
catch (error) {
|
|
1893
2478
|
harperLogger.warn?.('onCommit handler failed after commit', error);
|
|
1894
2479
|
}
|
|
1895
|
-
if (this.
|
|
2480
|
+
if (this.recordLocks)
|
|
2481
|
+
this.noteCommittedLockVersions(committedTransaction);
|
|
2482
|
+
releaseTransactionLockHolds(transaction);
|
|
2483
|
+
if (committedImmediate
|
|
2484
|
+
? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasSiblingCommitInFlight(committedTransaction)
|
|
2485
|
+
: !this.saveCommits || !this.hasSiblingCommitInFlight(committedTransaction))
|
|
2486
|
+
this.releaseRecordLocks();
|
|
2487
|
+
if (this.next && !committedImmediate) {
|
|
1896
2488
|
// never forward options.transaction (a retry/replay round's HEAD-store handle) to
|
|
1897
2489
|
// the next store — it must commit its own writes through its own transaction
|
|
1898
2490
|
let nextCommit;
|
|
@@ -1915,7 +2507,7 @@ class DatabaseTransaction {
|
|
|
1915
2507
|
// if we want to wait for replication confirmation, we need to track the transaction times
|
|
1916
2508
|
// and when replication notifications come in, we count the number of confirms until we reach the desired number
|
|
1917
2509
|
const databaseName = this.writes[0]?.store.rootStore.databaseName;
|
|
1918
|
-
const lastEntry = this.lastConfirmableEntry();
|
|
2510
|
+
const lastEntry = this.lastConfirmableEntry(true);
|
|
1919
2511
|
if (confirmReplication && databaseName && lastEntry) {
|
|
1920
2512
|
completions.push(confirmReplication(databaseName, lastEntry.version, this.replicatedConfirmation));
|
|
1921
2513
|
}
|
|
@@ -1926,24 +2518,36 @@ class DatabaseTransaction {
|
|
|
1926
2518
|
// later write retained survives). Deferred until here so a retry that *would* have
|
|
1927
2519
|
// referenced the blob can flip skipped/superseded back to false first.
|
|
1928
2520
|
for (const write of this.writes) {
|
|
1929
|
-
if (write?.
|
|
2521
|
+
if (!write?.saved || !write.savedBlobs)
|
|
2522
|
+
continue;
|
|
2523
|
+
if (committedImmediate
|
|
2524
|
+
? write.stagedOn !== committedTransaction
|
|
2525
|
+
: Boolean(write.stagedOn) && write.stagedOn !== committedTransaction)
|
|
2526
|
+
continue;
|
|
2527
|
+
if ((write.skipped &&
|
|
2528
|
+
!(write.blobsAuditReferenced &&
|
|
2529
|
+
(write.appendedTo === committedTransaction || hasDurableLog(write.appendedTo)))) ||
|
|
2530
|
+
(write.superseded && !write.blobsAuditReferenced))
|
|
1930
2531
|
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
1931
2532
|
}
|
|
1932
|
-
|
|
1933
|
-
this.noteCommittedLockVersions();
|
|
2533
|
+
const committedStagedTxnTime = this.getHandleStagedTxnTime(committedTransaction);
|
|
1934
2534
|
// now reset transactions tracking; this transaction be reused and committed again
|
|
1935
|
-
this.
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2535
|
+
this.clearWrites(committedTransaction);
|
|
2536
|
+
if (!committedImmediate) {
|
|
2537
|
+
this.retries = 0;
|
|
2538
|
+
if (options.doneWriting)
|
|
2539
|
+
this.endScopeOwnership();
|
|
2540
|
+
this.releaseContext(!!options.doneWriting);
|
|
2541
|
+
}
|
|
2542
|
+
let txnTime = committedStagedTxnTime || this.timestamp || 0;
|
|
2543
|
+
if ((!committedImmediate || this.open !== exports.TRANSACTION_STATE.OPEN) &&
|
|
2544
|
+
this.writes.length === 0 &&
|
|
2545
|
+
!this.hasActiveCommitHandle())
|
|
2546
|
+
this.timestamp = 0; // reset the timestamp as well
|
|
1943
2547
|
return Promise.all(completions).then(() => {
|
|
1944
2548
|
// Only once the chained store's commit has settled, as on the synchronous path: a
|
|
1945
2549
|
// partially failed mid-scope commit must not leave the scope resumable.
|
|
1946
|
-
this.completeMidScopeCommit(options);
|
|
2550
|
+
this.completeMidScopeCommit(options, committedTransaction);
|
|
1947
2551
|
return {
|
|
1948
2552
|
txnTime,
|
|
1949
2553
|
};
|
|
@@ -1951,10 +2555,14 @@ class DatabaseTransaction {
|
|
|
1951
2555
|
// As on the synchronous path: a completion that failed (a chained store's commit,
|
|
1952
2556
|
// a replication confirmation) leaves this commit partly landed, so ownership goes
|
|
1953
2557
|
// with it rather than letting a later commit rotate on top.
|
|
1954
|
-
|
|
2558
|
+
committedTransaction.committed = true;
|
|
2559
|
+
if (!committedImmediate)
|
|
2560
|
+
this.endScopeOwnership();
|
|
1955
2561
|
throw error;
|
|
1956
2562
|
});
|
|
1957
2563
|
}, (error) => {
|
|
2564
|
+
const txnWithRetry = transaction;
|
|
2565
|
+
this.setHandleCommitInFlight(transaction, false);
|
|
1958
2566
|
// Coordinated transactions surface conflicts as RETRY_NOW (handled in the
|
|
1959
2567
|
// resolve branch above) and never reach here with ERR_BUSY. But not every
|
|
1960
2568
|
// write transaction is coordinated — a write that reaches save() with no
|
|
@@ -1971,8 +2579,10 @@ class DatabaseTransaction {
|
|
|
1971
2579
|
if (error.code === 'ERR_BUSY' || error.code === 'ERR_TRY_AGAIN') {
|
|
1972
2580
|
// if the transaction failed due to concurrent changes, we need to retry. First record this as an increased risk of contention/retry
|
|
1973
2581
|
// for future transactions
|
|
1974
|
-
|
|
1975
|
-
|
|
2582
|
+
const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
|
|
2583
|
+
if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
|
|
2584
|
+
this.retries = retries;
|
|
2585
|
+
harperLogger.debug?.('retrying', transaction.id, retries);
|
|
1976
2586
|
// ERR_BUSY and ERR_TRY_AGAIN are both retried by recommitting the SAME native
|
|
1977
2587
|
// transaction. ERR_BUSY recovers because the save loop re-writes each key, re-tracking
|
|
1978
2588
|
// it at the current sequence. ERR_TRY_AGAIN — a snapshot stranded outside the memtable
|
|
@@ -1985,35 +2595,47 @@ class DatabaseTransaction {
|
|
|
1985
2595
|
// already-staged change-feed entry publishes only when the retry really commits — no
|
|
1986
2596
|
// premature publish, and no fresh-transaction replay that would drop the entry.
|
|
1987
2597
|
// Mark the native transaction as a retry so RocksTransactionLogStore skips re-staging entries.
|
|
1988
|
-
|
|
2598
|
+
txnWithRetry.isRetry = true;
|
|
2599
|
+
if (error?.hasLog === true) {
|
|
2600
|
+
txnWithRetry.hasStagedLog = true;
|
|
2601
|
+
}
|
|
2602
|
+
const logged = hasDurableLog(transaction, error);
|
|
1989
2603
|
// Before the backoff gate below: the budget can already be spent on the first
|
|
1990
2604
|
// retry when an earlier store in this chain consumed it.
|
|
1991
|
-
const pastBudget = this.elapsedPastCommitBudget();
|
|
1992
|
-
if (pastBudget)
|
|
1993
|
-
|
|
1994
|
-
|
|
2605
|
+
const pastBudget = this.elapsedPastCommitBudget(transaction);
|
|
2606
|
+
if (pastBudget) {
|
|
2607
|
+
if (logged)
|
|
2608
|
+
this.noteOverdueLoggedCommit(transaction, pastBudget);
|
|
2609
|
+
else
|
|
2610
|
+
this.abandonCommitAfterDeadline(transaction, pastBudget);
|
|
2611
|
+
}
|
|
2612
|
+
if (retries > 2) {
|
|
1995
2613
|
// Transactions applying data from a canonical source of truth (replication peer or
|
|
1996
2614
|
// external caching source) must never drop a write on a transient conflict: there is no
|
|
1997
2615
|
// re-subscribe / sequence-id-resume path, so a dropped write would leave this node
|
|
1998
2616
|
// permanently diverged (harper-pro#348). Such transactions retry without a cap; the source
|
|
1999
2617
|
// apply loop serializes commits (backpressure), so contention clears rather than
|
|
2000
|
-
// compounding.
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
}
|
|
2618
|
+
// compounding. Durably logged transactions likewise retry without a cap so a written
|
|
2619
|
+
// transaction-log entry is never abandoned.
|
|
2620
|
+
const neverDropOnConflict = this.sourceApply || logged;
|
|
2621
|
+
if (retries > MAX_RETRIES && !neverDropOnConflict) {
|
|
2622
|
+
// giving up: poison and abort the whole linked chain so no link leaks its native
|
|
2623
|
+
// handle / read snapshot until GC.
|
|
2624
|
+
this.abortChainAfterRetries(transaction);
|
|
2625
|
+
throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
|
|
2626
|
+
}
|
|
2627
|
+
// Uncapped retry can otherwise stall silently (debug logging is off in production);
|
|
2628
|
+
// surface periodic visibility into a stalled source-apply or logged commit.
|
|
2629
|
+
if (neverDropOnConflict && retries % MAX_RETRIES === 0) {
|
|
2630
|
+
harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} retries; continuing to retry`);
|
|
2014
2631
|
}
|
|
2015
2632
|
// start delaying, back off to try to space out transactions and avoid excessive conflicts
|
|
2016
|
-
|
|
2633
|
+
const backoff = (0, promises_1.setTimeout)(Math.min(retries * retries, MAX_RETRY_DELAY_MS));
|
|
2634
|
+
if (logged) {
|
|
2635
|
+
// waitForDrain() must outlast the backoff too
|
|
2636
|
+
trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
|
|
2637
|
+
}
|
|
2638
|
+
return backoff.then(() => this.commit({ ...options, transaction }));
|
|
2017
2639
|
}
|
|
2018
2640
|
return this.commit({ ...options, transaction }); // try again
|
|
2019
2641
|
}
|
|
@@ -2021,54 +2643,87 @@ class DatabaseTransaction {
|
|
|
2021
2643
|
// terminal (non-conflict) failure: release the native handle so it doesn't leak;
|
|
2022
2644
|
// usually already released by the failed commit itself, abort for the unexpected
|
|
2023
2645
|
// case (same defensive pattern as the retry-exhaustion give-up above)
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
catch (abortError) {
|
|
2028
|
-
harperLogger.debug?.('aborting transaction after failed commit', abortError);
|
|
2646
|
+
if (error?.hasLog) {
|
|
2647
|
+
txnWithRetry.isRetry = true;
|
|
2648
|
+
txnWithRetry.hasStagedLog = true;
|
|
2029
2649
|
}
|
|
2650
|
+
abortNativeTransaction(transaction, 'aborting transaction after failed commit', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2030
2651
|
// A terminal failure is just as final as a success — release the context's
|
|
2031
2652
|
// back-reference while handling the native transaction. performCommit's outer
|
|
2032
2653
|
// rejection handler then aborts the wrapper and its linked chain.
|
|
2033
2654
|
// A failed commit must never be followed by a resumed segment: this generation is
|
|
2034
2655
|
// finished and its durability is unknown, so ownership goes with it.
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2656
|
+
if (!isImmediateCommit &&
|
|
2657
|
+
!this.hasWriteOwnedByActiveCommit(transaction, this.hasSiblingCommitInFlight(transaction), true, true)) {
|
|
2658
|
+
this.endScopeOwnership();
|
|
2659
|
+
this.releaseContext(!!options.doneWriting);
|
|
2660
|
+
}
|
|
2661
|
+
if (!hasDurableLog(transaction, error)) {
|
|
2662
|
+
for (const write of this.writes) {
|
|
2663
|
+
if (!write?.saved ||
|
|
2664
|
+
!write.savedBlobs ||
|
|
2665
|
+
(isImmediateCommit
|
|
2666
|
+
? write.stagedOn !== transaction
|
|
2667
|
+
: Boolean(write.stagedOn) && write.stagedOn !== transaction))
|
|
2668
|
+
continue;
|
|
2669
|
+
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
this.clearWrites(transaction);
|
|
2673
|
+
releaseTransactionLockHolds(transaction);
|
|
2674
|
+
if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
|
|
2675
|
+
!this.hasActiveCommitHandle() &&
|
|
2676
|
+
this.writes.length === 0)
|
|
2677
|
+
this.releaseRecordLocks();
|
|
2678
|
+
if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle())
|
|
2679
|
+
this.timestamp = 0;
|
|
2039
2680
|
throw error;
|
|
2040
2681
|
}
|
|
2041
2682
|
});
|
|
2042
|
-
//
|
|
2043
|
-
//
|
|
2044
|
-
//
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
chainRoot.commitStartedAt = undefined;
|
|
2050
|
-
return resolution;
|
|
2051
|
-
}, (error) => {
|
|
2052
|
-
chainRoot.commitStartedAt = undefined;
|
|
2683
|
+
// Release any lock holds remaining if `commitOutcome` rejects (for example when a chained
|
|
2684
|
+
// store's commit or a completion fails) through the returned promise rather than a second
|
|
2685
|
+
// subscriber that would mark an unawaited commit rejection as handled.
|
|
2686
|
+
if (!hadLeaseProtectedWrite)
|
|
2687
|
+
return commitOutcome;
|
|
2688
|
+
return commitOutcome.then(undefined, (error) => {
|
|
2689
|
+
releaseTransactionLockHolds(transaction);
|
|
2053
2690
|
throw error;
|
|
2054
2691
|
});
|
|
2055
2692
|
}
|
|
2056
2693
|
for (const write of this.writes) {
|
|
2057
|
-
if (write?.
|
|
2694
|
+
if (!write?.saved)
|
|
2695
|
+
continue;
|
|
2696
|
+
if (isImmediateCommit
|
|
2697
|
+
? write.stagedOn !== transaction
|
|
2698
|
+
: this.hasSiblingCommitInFlight(transaction) && Boolean(write.stagedOn) && write.stagedOn !== transaction)
|
|
2699
|
+
continue;
|
|
2700
|
+
if (write.savedBlobs &&
|
|
2701
|
+
((write.skipped &&
|
|
2702
|
+
!(write.blobsAuditReferenced && (write.appendedTo === transaction || hasDurableLog(write.appendedTo)))) ||
|
|
2703
|
+
(write.superseded && !write.blobsAuditReferenced)))
|
|
2058
2704
|
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
2059
2705
|
}
|
|
2060
2706
|
if (this.recordLocks)
|
|
2061
|
-
this.noteCommittedLockVersions();
|
|
2062
|
-
this.
|
|
2063
|
-
this.
|
|
2064
|
-
if (
|
|
2065
|
-
this.
|
|
2066
|
-
|
|
2707
|
+
this.noteCommittedLockVersions(transaction);
|
|
2708
|
+
const committedStagedTxnTime = this.getHandleStagedTxnTime(transaction);
|
|
2709
|
+
this.clearWrites(transaction);
|
|
2710
|
+
if (isImmediateCommit
|
|
2711
|
+
? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle() && this.writes.length === 0
|
|
2712
|
+
: !this.saveCommits || (!this.hasActiveCommitHandle() && this.writes.length === 0))
|
|
2713
|
+
this.releaseRecordLocks();
|
|
2714
|
+
if (!isImmediateCommit) {
|
|
2715
|
+
if (options.doneWriting)
|
|
2716
|
+
this.endScopeOwnership();
|
|
2717
|
+
this.releaseContext(!!options.doneWriting);
|
|
2718
|
+
}
|
|
2067
2719
|
const txnResolution = {
|
|
2068
|
-
txnTime: this.timestamp,
|
|
2720
|
+
txnTime: committedStagedTxnTime || this.timestamp,
|
|
2069
2721
|
};
|
|
2070
|
-
|
|
2071
|
-
|
|
2722
|
+
if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
|
|
2723
|
+
this.writes.length === 0 &&
|
|
2724
|
+
!this.hasActiveCommitHandle())
|
|
2725
|
+
this.timestamp = 0;
|
|
2726
|
+
if (this.next && !isImmediateCommit) {
|
|
2072
2727
|
// now run any other transactions
|
|
2073
2728
|
options.timestamp = txnResolution.txnTime;
|
|
2074
2729
|
// as above: the next store must not inherit this store's explicit native transaction
|
|
@@ -2087,7 +2742,7 @@ class DatabaseTransaction {
|
|
|
2087
2742
|
return nextResolution?.then((nextResolution) => {
|
|
2088
2743
|
// Only once the chained store's own commit has SETTLED: rotating first would leave the
|
|
2089
2744
|
// scope resumable after a partially failed mid-scope commit.
|
|
2090
|
-
this.completeMidScopeCommit(options);
|
|
2745
|
+
this.completeMidScopeCommit(options, transaction);
|
|
2091
2746
|
return {
|
|
2092
2747
|
txnTime: txnResolution.txnTime,
|
|
2093
2748
|
next: nextResolution,
|
|
@@ -2102,18 +2757,20 @@ class DatabaseTransaction {
|
|
|
2102
2757
|
});
|
|
2103
2758
|
txnResolution.next = nextResolution;
|
|
2104
2759
|
}
|
|
2105
|
-
this.completeMidScopeCommit(options);
|
|
2760
|
+
this.completeMidScopeCommit(options, transaction);
|
|
2106
2761
|
return txnResolution;
|
|
2107
2762
|
});
|
|
2108
2763
|
}
|
|
2109
2764
|
catch (error) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2765
|
+
if (inCommitPhase)
|
|
2766
|
+
this.setCommitPhase(false);
|
|
2767
|
+
this.abortAfterCommitError(error, transaction);
|
|
2112
2768
|
}
|
|
2113
2769
|
if (commitResult?.then)
|
|
2114
2770
|
return commitResult.catch((error) => {
|
|
2115
|
-
|
|
2116
|
-
|
|
2771
|
+
if (inCommitPhase)
|
|
2772
|
+
this.setCommitPhase(false);
|
|
2773
|
+
this.abortAfterCommitError(error, transaction);
|
|
2117
2774
|
});
|
|
2118
2775
|
return commitResult;
|
|
2119
2776
|
}
|
|
@@ -2141,8 +2798,12 @@ class DatabaseTransaction {
|
|
|
2141
2798
|
this.#scopeOwned = false;
|
|
2142
2799
|
this.snapshotFree = false;
|
|
2143
2800
|
}
|
|
2144
|
-
completeMidScopeCommit(options) {
|
|
2801
|
+
completeMidScopeCommit(options, committedTransaction) {
|
|
2802
|
+
if (committedTransaction?.immediateCommit)
|
|
2803
|
+
return;
|
|
2145
2804
|
this.next = null;
|
|
2805
|
+
if (this.saveCommits && this.hasActiveCommitHandle())
|
|
2806
|
+
return;
|
|
2146
2807
|
this.rotateAfterMidScopeCommit(options);
|
|
2147
2808
|
}
|
|
2148
2809
|
/** See completeMidScopeCommit, which is the only caller and carries the reasoning. */
|
|
@@ -2157,82 +2818,147 @@ class DatabaseTransaction {
|
|
|
2157
2818
|
this.open = exports.TRANSACTION_STATE.OPEN;
|
|
2158
2819
|
this.snapshotFree = true;
|
|
2159
2820
|
this.writesAbandoned = false;
|
|
2821
|
+
if (this.#context && (this.#context.transaction === this || this.#context.transaction === exports.RELEASED_TRANSACTION)) {
|
|
2822
|
+
this.#context.transaction = this;
|
|
2823
|
+
}
|
|
2160
2824
|
}
|
|
2161
2825
|
assertDatabaseCommitAllowed(transaction) {
|
|
2826
|
+
if (hasDurableLog(transaction))
|
|
2827
|
+
return;
|
|
2162
2828
|
const rootStore = this.writes[0]?.store?.rootStore;
|
|
2163
2829
|
if (!databaseCommitsSuspended(rootStore))
|
|
2164
2830
|
return;
|
|
2165
2831
|
const retryable = !this.root && !this.snapshotFree;
|
|
2166
2832
|
if (this.transaction === transaction)
|
|
2167
2833
|
this.detachOwnedTransaction();
|
|
2168
|
-
abortNativeTransaction(transaction, 'aborting a transaction while its database is closing');
|
|
2834
|
+
abortNativeTransaction(transaction, 'aborting a transaction while its database is closing', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2169
2835
|
try {
|
|
2170
|
-
this.abort();
|
|
2836
|
+
this.abort(false, true, transaction);
|
|
2171
2837
|
}
|
|
2172
2838
|
catch (error) {
|
|
2173
2839
|
harperLogger.debug?.('cleaning up a transaction while its database is closing', error);
|
|
2174
2840
|
}
|
|
2175
2841
|
throw new hdbError_ts_1.DatabaseClosingError(rootStore?.databaseName ?? rootStore?.path ?? 'unknown', retryable);
|
|
2176
2842
|
}
|
|
2177
|
-
abortAfterCommitError(error) {
|
|
2843
|
+
abortAfterCommitError(error, targetTransaction) {
|
|
2178
2844
|
try {
|
|
2845
|
+
const retryTxn = targetTransaction;
|
|
2846
|
+
const retryTxnCommitted = this.isHandleCommitted(retryTxn);
|
|
2847
|
+
const retryTxnInFlight = this.isHandleCommitInFlight(retryTxn);
|
|
2848
|
+
if (retryTxn && retryTxnCommitted)
|
|
2849
|
+
retryTxn.committed = true;
|
|
2850
|
+
if (retryTxn)
|
|
2851
|
+
this.releaseActiveCommitHandle(retryTxn);
|
|
2852
|
+
if (retryTxn && retryTxn !== this.transaction && !retryTxnCommitted && !retryTxnInFlight) {
|
|
2853
|
+
releaseTransactionLockHolds(retryTxn);
|
|
2854
|
+
abortNativeTransaction(retryTxn, 'aborting a retry transaction after pre-commit failure', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2855
|
+
}
|
|
2179
2856
|
// Not "always retain": abort(true) keeps the native handle only while read iterators still
|
|
2180
2857
|
// own it. resources/transaction.ts's own post-error abort passes the same argument, so the
|
|
2181
2858
|
// two layers cannot contradict each other one frame apart.
|
|
2182
|
-
this.abort(true);
|
|
2859
|
+
this.abort(true, true, targetTransaction);
|
|
2183
2860
|
}
|
|
2184
2861
|
catch (abortError) {
|
|
2185
2862
|
harperLogger.debug?.('aborting transaction after a failed commit', abortError);
|
|
2186
2863
|
}
|
|
2187
2864
|
throw error;
|
|
2188
2865
|
}
|
|
2189
|
-
abort(retainReadTransaction = false, cascade = true) {
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2866
|
+
abort(retainReadTransaction = false, cascade = true, targetTransaction) {
|
|
2867
|
+
const nativeTxn = targetTransaction ?? this.transaction;
|
|
2868
|
+
const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
|
|
2869
|
+
const hasSiblingInFlight = this.hasSiblingCommitInFlight(targetTransaction);
|
|
2870
|
+
const preserveActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true);
|
|
2871
|
+
const preserveNonImmediateActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true, true);
|
|
2872
|
+
const cascadeNext = cascade && !preserveNonImmediateActiveWrites;
|
|
2873
|
+
const next = cascadeNext ? this.next : undefined;
|
|
2874
|
+
if (cascadeNext && !retainReadTransaction)
|
|
2192
2875
|
this.next = null;
|
|
2193
2876
|
try {
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
this.
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
this.
|
|
2877
|
+
const ownsCurrentTransaction = !isImmediateTarget && (!hasSiblingInFlight || !targetTransaction || this.transaction === targetTransaction);
|
|
2878
|
+
const currentTxn = this.transaction;
|
|
2879
|
+
const currentTxnActive = Boolean(currentTxn) &&
|
|
2880
|
+
!currentTxn.abortedByWrapper &&
|
|
2881
|
+
!this.isHandleCommitted(currentTxn) &&
|
|
2882
|
+
(this.isHandleCommitInFlight(currentTxn) ||
|
|
2883
|
+
hasDurableLog(currentTxn) ||
|
|
2884
|
+
(Boolean(targetTransaction) && currentTxn !== targetTransaction && this.isActiveCommitHandle(currentTxn)));
|
|
2885
|
+
if (ownsCurrentTransaction && !currentTxnActive) {
|
|
2886
|
+
if (this.transaction)
|
|
2887
|
+
this.releaseActiveCommitHandle(this.transaction);
|
|
2888
|
+
const hasOpenReadIterator = retainReadTransaction &&
|
|
2889
|
+
this.transaction &&
|
|
2890
|
+
(this.readTxnsUsed > 1 || (this.baseReadRefConsumed && this.readTxnsUsed > 0));
|
|
2891
|
+
if (hasOpenReadIterator) {
|
|
2892
|
+
this.releaseRetainedWriteIntents();
|
|
2893
|
+
if (!this.baseReadRefConsumed) {
|
|
2894
|
+
this.doneReadTxn();
|
|
2895
|
+
this.baseReadRefConsumed = true;
|
|
2896
|
+
}
|
|
2202
2897
|
}
|
|
2898
|
+
else {
|
|
2899
|
+
while (this.transaction && this.readTxnsUsed > 0)
|
|
2900
|
+
this.doneReadTxn(); // release the read snapshot when we abort, we assume we don't need it
|
|
2901
|
+
}
|
|
2902
|
+
// A write-only transaction never took a read reference (getReadTxn was never called), so the
|
|
2903
|
+
// loop above releases nothing even though save() created a native handle. Keep an iterator's
|
|
2904
|
+
// handle alive, but release every other remaining handle through the shared cleanup path.
|
|
2905
|
+
if (this.transaction && !hasOpenReadIterator)
|
|
2906
|
+
this.releaseReadTxn();
|
|
2203
2907
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
// A write-only transaction never took a read reference (getReadTxn was never called), so the
|
|
2209
|
-
// loop above releases nothing even though save() created a native handle. Keep an iterator's
|
|
2210
|
-
// handle alive, but release every other remaining handle through the shared cleanup path.
|
|
2211
|
-
if (this.transaction && !hasOpenReadIterator)
|
|
2212
|
-
this.releaseReadTxn();
|
|
2213
|
-
this.open = exports.TRANSACTION_STATE.CLOSED;
|
|
2214
|
-
this.timestamp = 0; // a lock stamp pinned for this write set must not leak into the next
|
|
2215
|
-
this.drainCompletions();
|
|
2908
|
+
if (!isImmediateTarget)
|
|
2909
|
+
this.open = exports.TRANSACTION_STATE.CLOSED;
|
|
2910
|
+
this.drainCompletions(targetTransaction, preserveActiveWrites);
|
|
2911
|
+
const targetLogged = hasDurableLog(targetTransaction);
|
|
2216
2912
|
for (const write of this.writes) {
|
|
2217
|
-
if (write
|
|
2913
|
+
if (!write)
|
|
2914
|
+
continue;
|
|
2915
|
+
if (preserveActiveWrites && this.isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, true))
|
|
2916
|
+
continue;
|
|
2917
|
+
if (targetLogged &&
|
|
2918
|
+
write.saved &&
|
|
2919
|
+
(isImmediateTarget
|
|
2920
|
+
? write.stagedOn === targetTransaction
|
|
2921
|
+
: !write.stagedOn || write.stagedOn === targetTransaction))
|
|
2922
|
+
continue;
|
|
2923
|
+
if (write.savedBlobs)
|
|
2218
2924
|
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
2219
2925
|
}
|
|
2220
2926
|
}
|
|
2221
2927
|
finally {
|
|
2222
2928
|
try {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2929
|
+
if (!isImmediateTarget)
|
|
2930
|
+
this.open = exports.TRANSACTION_STATE.CLOSED;
|
|
2931
|
+
const endScopeOnAbort = !isImmediateTarget && (!targetTransaction || !preserveNonImmediateActiveWrites);
|
|
2932
|
+
if (endScopeOnAbort)
|
|
2933
|
+
this.endScopeOwnership(); // the scope is over; nothing may rotate this instance again
|
|
2934
|
+
this.clearWrites(targetTransaction, true);
|
|
2935
|
+
if (targetTransaction) {
|
|
2936
|
+
releaseTransactionLockHolds(targetTransaction);
|
|
2937
|
+
}
|
|
2938
|
+
if ((!isImmediateTarget || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle()) {
|
|
2939
|
+
this.timestamp = 0; // a lock stamp pinned for this write set must not leak into the next
|
|
2940
|
+
this.retries = 0;
|
|
2941
|
+
}
|
|
2942
|
+
if ((isImmediateTarget
|
|
2943
|
+
? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle()
|
|
2944
|
+
: !preserveActiveWrites) &&
|
|
2945
|
+
this.writes.length === 0) {
|
|
2946
|
+
releaseTransactionLockHolds(nativeTxn);
|
|
2947
|
+
if (this.transaction && this.transaction !== nativeTxn)
|
|
2948
|
+
releaseTransactionLockHolds(this.transaction);
|
|
2949
|
+
this.releaseRecordLocks();
|
|
2950
|
+
}
|
|
2227
2951
|
// A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
|
|
2228
2952
|
// Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
|
|
2229
2953
|
// starting a fresh one) so the rest of the logical operation fails atomically via the
|
|
2230
2954
|
// poison check in addWrite()/commit(), rather than silently landing a later write on a
|
|
2231
2955
|
// brand-new transaction after an earlier one was rolled back (#1411). Releasing here would
|
|
2232
2956
|
// make that check see `undefined?.timedOut` and take the "start fresh" branch instead.
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
this.
|
|
2957
|
+
if (endScopeOnAbort) {
|
|
2958
|
+
this.releaseContext(!this.timedOut && !this.disconnected);
|
|
2959
|
+
if (!this.root)
|
|
2960
|
+
this.releaseRequestAbortListener();
|
|
2961
|
+
}
|
|
2236
2962
|
}
|
|
2237
2963
|
finally {
|
|
2238
2964
|
if (next && !(next.nativeCommitSubmitted && next.commitsInFlight)) {
|
|
@@ -2259,10 +2985,10 @@ class DatabaseTransaction {
|
|
|
2259
2985
|
* Source-applied writes are exempt for the same reason they are exempt from the attempt cap:
|
|
2260
2986
|
* there is no resubscribe/sequence-resume path, so dropping one permanently diverges this node.
|
|
2261
2987
|
*/
|
|
2262
|
-
elapsedPastCommitBudget() {
|
|
2988
|
+
elapsedPastCommitBudget(transaction) {
|
|
2263
2989
|
if (this.sourceApply)
|
|
2264
2990
|
return 0;
|
|
2265
|
-
const startedAt =
|
|
2991
|
+
const startedAt = this.budgetCarrier(transaction).commitStartedAt;
|
|
2266
2992
|
if (startedAt == null)
|
|
2267
2993
|
return 0;
|
|
2268
2994
|
const elapsed = performance.now() - startedAt;
|
|
@@ -2283,18 +3009,54 @@ class DatabaseTransaction {
|
|
|
2283
3009
|
const elapsed = Math.round(elapsedMs);
|
|
2284
3010
|
const budget = this.commitConflictBudget();
|
|
2285
3011
|
const retryable = !this.root && !this.snapshotFree;
|
|
3012
|
+
const retries = headTransaction?.retries ?? this.retries;
|
|
2286
3013
|
if (allowStuckCommitLog('abandon', performance.now())) {
|
|
2287
|
-
|
|
2288
|
-
|
|
3014
|
+
let headTransactionId;
|
|
3015
|
+
try {
|
|
3016
|
+
headTransactionId = headTransaction?.id;
|
|
3017
|
+
}
|
|
3018
|
+
catch { }
|
|
3019
|
+
harperLogger.error?.(`Abandoning a write transaction: its commit has been in write-intent conflict for ${elapsed}ms ` +
|
|
3020
|
+
`(exceeds the ${budget}ms limit) across ${retries} retries, ` +
|
|
2289
3021
|
describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
|
|
2290
3022
|
`.` +
|
|
2291
|
-
(0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path,
|
|
3023
|
+
(0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
|
|
2292
3024
|
` Another transaction holds a conflicting write intent and has not completed; the request is ` +
|
|
2293
3025
|
`failed with a 503${retryable ? '' : ' (not retryable — an earlier store in this transaction already committed)'} ` +
|
|
2294
3026
|
`rather than waiting further.`);
|
|
2295
3027
|
}
|
|
2296
3028
|
this.abortChainAfterRetries(headTransaction);
|
|
2297
|
-
throw new hdbError_ts_1.TransactionCommitConflictTimeoutError(`Commit was in conflict with ongoing writes for ${elapsed}ms, exceeding the ${budget}ms limit; transaction abandoned after ${
|
|
3029
|
+
throw new hdbError_ts_1.TransactionCommitConflictTimeoutError(`Commit was in conflict with ongoing writes for ${elapsed}ms, exceeding the ${budget}ms limit; transaction abandoned after ${retries} retries`, retryable);
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* A durably logged commit past its conflict budget cannot be aborted; release its deferred
|
|
3033
|
+
* record-lock holds (`holdBudgetSpent` prevents re-retaining them) and log once per chain/cooldown.
|
|
3034
|
+
*/
|
|
3035
|
+
noteOverdueLoggedCommit(headTransaction, elapsedMs) {
|
|
3036
|
+
headTransaction.holdBudgetSpent = true;
|
|
3037
|
+
releaseTransactionLockHolds(headTransaction);
|
|
3038
|
+
const carrier = this.budgetCarrier(headTransaction);
|
|
3039
|
+
if (carrier.commitOverdueLogged)
|
|
3040
|
+
return;
|
|
3041
|
+
if (!allowStuckCommitLog('overdue', performance.now()))
|
|
3042
|
+
return;
|
|
3043
|
+
carrier.commitOverdueLogged = true;
|
|
3044
|
+
const elapsed = Math.round(elapsedMs);
|
|
3045
|
+
const budget = this.commitConflictBudget();
|
|
3046
|
+
const retries = headTransaction?.retries ?? this.retries;
|
|
3047
|
+
let headTransactionId;
|
|
3048
|
+
try {
|
|
3049
|
+
headTransactionId = headTransaction?.id;
|
|
3050
|
+
}
|
|
3051
|
+
catch { }
|
|
3052
|
+
harperLogger.error?.(`A durably logged write transaction is still in write-intent conflict after ${elapsed}ms ` +
|
|
3053
|
+
`(exceeds the ${budget}ms limit) across ${retries} retries, ` +
|
|
3054
|
+
describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
|
|
3055
|
+
`.` +
|
|
3056
|
+
(0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
|
|
3057
|
+
` Because its transaction-log entry is already durable on disk, it cannot be aborted and ` +
|
|
3058
|
+
`will continue retrying until the conflicting write intent releases (pinning the local ` +
|
|
3059
|
+
`transaction-log read watermark for subscriptions and replication while uncommitted).`);
|
|
2298
3060
|
}
|
|
2299
3061
|
/**
|
|
2300
3062
|
* Give up on a chain of linked transactions after exhausting conflict retries: poison every link
|
|
@@ -2307,36 +3069,51 @@ class DatabaseTransaction {
|
|
|
2307
3069
|
* from the replay transaction; retry exhaustion aborts both.
|
|
2308
3070
|
*/
|
|
2309
3071
|
abortChainAfterRetries(headTransaction) {
|
|
3072
|
+
const isImmediateCommit = Boolean(headTransaction?.immediateCommit);
|
|
3073
|
+
const hasSiblingInFlight = this.hasSiblingCommitInFlight(headTransaction);
|
|
3074
|
+
if (isImmediateCommit ||
|
|
3075
|
+
(hasSiblingInFlight && this.hasWriteOwnedByActiveCommit(headTransaction, hasSiblingInFlight, true, true))) {
|
|
3076
|
+
if (!isImmediateCommit && this.transaction === headTransaction)
|
|
3077
|
+
this.detachOwnedTransaction();
|
|
3078
|
+
releaseTransactionLockHolds(headTransaction);
|
|
3079
|
+
abortNativeTransaction(headTransaction, 'aborting conflicted transaction in chain after exhausting retries', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
3080
|
+
try {
|
|
3081
|
+
this.abort(false, false, headTransaction);
|
|
3082
|
+
}
|
|
3083
|
+
catch (abortError) {
|
|
3084
|
+
harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
|
|
3085
|
+
}
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
2310
3088
|
for (let txn = this; txn; txn = txn.next) {
|
|
2311
3089
|
txn.open = exports.TRANSACTION_STATE.CLOSED;
|
|
2312
3090
|
}
|
|
2313
|
-
for (let txn = this; txn; txn =
|
|
3091
|
+
for (let txn = this, next; txn; txn = next) {
|
|
3092
|
+
next = txn.next;
|
|
3093
|
+
txn.next = null;
|
|
2314
3094
|
// Detach first so the abort() below performs only non-native cleanup, and so its doneReadTxn
|
|
2315
3095
|
// loop cannot spin on a nulled handle. Not to avoid a double abort: rocksdb-js tolerates
|
|
2316
3096
|
// abort-after-abort, and it is abort-after-COMMIT that throws.
|
|
2317
|
-
const detached = txn
|
|
3097
|
+
const detached = txn === this &&
|
|
3098
|
+
hasSiblingInFlight &&
|
|
3099
|
+
txn.transaction &&
|
|
3100
|
+
txn.transaction !== headTransaction &&
|
|
3101
|
+
(this.isActiveCommitHandle(txn.transaction) || hasDurableLog(txn.transaction))
|
|
3102
|
+
? null
|
|
3103
|
+
: txn.detachOwnedTransaction();
|
|
2318
3104
|
const committingTransaction = txn === this ? headTransaction : detached;
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
}
|
|
2322
|
-
catch (abortError) {
|
|
2323
|
-
harperLogger.debug?.('aborting conflicted transaction in chain after exhausting retries', abortError);
|
|
2324
|
-
}
|
|
3105
|
+
releaseTransactionLockHolds(committingTransaction);
|
|
3106
|
+
abortNativeTransaction(committingTransaction, 'aborting conflicted transaction in chain after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
|
|
2325
3107
|
// With outstanding iterators, the head owns a retained read handle while the retry commits
|
|
2326
3108
|
// through a separate replay handle. Both must be aborted after retry exhaustion.
|
|
2327
3109
|
if (txn === this && detached && detached !== committingTransaction) {
|
|
2328
|
-
|
|
2329
|
-
detached.abort();
|
|
2330
|
-
}
|
|
2331
|
-
catch (abortError) {
|
|
2332
|
-
harperLogger.debug?.('aborting retained read transaction after exhausting retries', abortError);
|
|
2333
|
-
}
|
|
3110
|
+
abortNativeTransaction(detached, 'aborting retained read transaction after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
|
|
2334
3111
|
}
|
|
2335
3112
|
try {
|
|
2336
3113
|
// abort() synchronously walks savedBlobs and can call write.store.getEntry(), which can throw
|
|
2337
3114
|
// (closed store, decode error). Catch and continue so one link's wrapper-cleanup failure can't
|
|
2338
3115
|
// strand later links' native handles — they were already detached/aborted above regardless.
|
|
2339
|
-
txn.abort();
|
|
3116
|
+
txn.abort(false, false, committingTransaction);
|
|
2340
3117
|
}
|
|
2341
3118
|
catch (abortError) {
|
|
2342
3119
|
harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
|
|
@@ -2430,18 +3207,7 @@ class DatabaseTransaction {
|
|
|
2430
3207
|
*/
|
|
2431
3208
|
poisonAfterStalledSubmittedCommit() {
|
|
2432
3209
|
const root = this.root ?? this;
|
|
2433
|
-
const
|
|
2434
|
-
const collect = (start) => {
|
|
2435
|
-
for (let txn = start; txn; txn = txn.next)
|
|
2436
|
-
links.add(txn);
|
|
2437
|
-
};
|
|
2438
|
-
collect(root);
|
|
2439
|
-
collect(this);
|
|
2440
|
-
collect(root.submittedLink);
|
|
2441
|
-
if (root.submittedLinks)
|
|
2442
|
-
for (const txn of root.submittedLinks)
|
|
2443
|
-
collect(txn);
|
|
2444
|
-
for (const txn of links) {
|
|
3210
|
+
for (const txn of collectCommitLinks(root, this)) {
|
|
2445
3211
|
txn.postSubmitPoisoned = true;
|
|
2446
3212
|
// Not on a link an earlier poison already aborted: abort() cleared its write set, so excusing
|
|
2447
3213
|
// its continuation from the poison check would let it resume onto nothing and report success.
|
|
@@ -2465,7 +3231,7 @@ class DatabaseTransaction {
|
|
|
2465
3231
|
// throws. abort() rather than the native abort alone: it reclaims blobs a replayed write
|
|
2466
3232
|
// staged.
|
|
2467
3233
|
this.detachOwnedTransaction();
|
|
2468
|
-
abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed');
|
|
3234
|
+
abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2469
3235
|
try {
|
|
2470
3236
|
this.abort();
|
|
2471
3237
|
}
|
|
@@ -2505,10 +3271,12 @@ class ImmediateTransaction extends DatabaseTransaction {
|
|
|
2505
3271
|
}
|
|
2506
3272
|
save(...args) {
|
|
2507
3273
|
const operation = args[0]; // the staged write, not a transaction — commit() re-enters here with it
|
|
2508
|
-
if (
|
|
3274
|
+
if (operation?.deferSave)
|
|
3275
|
+
operation.deferSave = false;
|
|
3276
|
+
if (this.isCommitting || args[1] || args[3]) {
|
|
2509
3277
|
// Stage into the transaction commit() is committing (on a retry round, the one being retried)
|
|
2510
3278
|
// and reload within it; with no handle super.save() would open its own and nest a commit().
|
|
2511
|
-
super.save(operation, args[1], true);
|
|
3279
|
+
super.save(operation, args[1], true, args[3]);
|
|
2512
3280
|
}
|
|
2513
3281
|
else {
|
|
2514
3282
|
this.admitRequestWrite(); // a deferred save reaches here without passing through addWrite()
|
|
@@ -2542,7 +3310,7 @@ class ImmediateTransaction extends DatabaseTransaction {
|
|
|
2542
3310
|
return;
|
|
2543
3311
|
for (const [store, storeMap] of recordLocks) {
|
|
2544
3312
|
for (const [keyId, handle] of storeMap)
|
|
2545
|
-
if (handle.released)
|
|
3313
|
+
if (handle.released || handle.isExpired())
|
|
2546
3314
|
storeMap.delete(keyId);
|
|
2547
3315
|
if (storeMap.size === 0)
|
|
2548
3316
|
recordLocks.delete(store);
|
|
@@ -2704,6 +3472,12 @@ function reportIfLongLived(txn, thresholdMs, now, reportBudget) {
|
|
|
2704
3472
|
/** Diagnostic grace after a native submission outlives the ordinary transaction timeout. */
|
|
2705
3473
|
const MAX_DEFERRED_POISON_TICKS = 2;
|
|
2706
3474
|
const STALLED_COMMIT_LOG_MIN_INTERVAL_MS = 1000;
|
|
3475
|
+
// A logged retry parked on a write intent moves its retry counter at least every native park timeout,
|
|
3476
|
+
// so a counter that has not moved for two of them is a native attempt that never settled — whatever
|
|
3477
|
+
// the monitor's tick interval (STORAGE_MAXTRANSACTIONOPENTIME can be set far below the park timeout).
|
|
3478
|
+
const configuredParkTimeoutMs = Number(process.env.ROCKSDB_JS_PARK_TIMEOUT_MS);
|
|
3479
|
+
// Same acceptance rule as rocksdb-js: anything but a finite positive number falls back to its 5000 ms default.
|
|
3480
|
+
const LOGGED_RETRY_STALL_MS = 2 * (Number.isFinite(configuredParkTimeoutMs) && configuredParkTimeoutMs > 0 ? configuredParkTimeoutMs : 5000);
|
|
2707
3481
|
let lastStalledCommitWarningAt = -Infinity;
|
|
2708
3482
|
let lastStalledCommitErrorAt = -Infinity;
|
|
2709
3483
|
function submittedCommitStillInFlight(root) {
|
|
@@ -2717,12 +3491,36 @@ function submittedCommitStillInFlight(root) {
|
|
|
2717
3491
|
}
|
|
2718
3492
|
return false;
|
|
2719
3493
|
}
|
|
3494
|
+
// Every link the monitor would poison for a stalled commit, so the two walks agree on which links count.
|
|
3495
|
+
function collectCommitLinks(root, txn) {
|
|
3496
|
+
const links = new Set();
|
|
3497
|
+
const collect = (start) => {
|
|
3498
|
+
for (let link = start; link; link = link.next)
|
|
3499
|
+
links.add(link);
|
|
3500
|
+
};
|
|
3501
|
+
collect(root);
|
|
3502
|
+
collect(txn);
|
|
3503
|
+
collect(root.submittedLink);
|
|
3504
|
+
if (root.submittedLinks)
|
|
3505
|
+
for (const link of root.submittedLinks)
|
|
3506
|
+
collect(link);
|
|
3507
|
+
return links;
|
|
3508
|
+
}
|
|
3509
|
+
function chainDurableLogRetries(root, txn) {
|
|
3510
|
+
let retries = -1;
|
|
3511
|
+
for (const link of collectCommitLinks(root, txn)) {
|
|
3512
|
+
const linkRetries = link.activeDurableLogRetries();
|
|
3513
|
+
if (linkRetries >= 0)
|
|
3514
|
+
retries = Math.max(retries, 0) + linkRetries;
|
|
3515
|
+
}
|
|
3516
|
+
return retries;
|
|
3517
|
+
}
|
|
2720
3518
|
/**
|
|
2721
3519
|
* True when this tick must leave `txn` alone because some link has submitted writes whose native
|
|
2722
3520
|
* outcome is not known yet. Merely entering commit() is not enough: a wholly pre-submit transaction
|
|
2723
3521
|
* remains subject to shouldSpareCommitPhase() and its ordinary bounded grace.
|
|
2724
3522
|
*/
|
|
2725
|
-
function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
|
|
3523
|
+
function deferForCommitInFlight(txn, url, expiration = txnExpiration, tick = monitorTick) {
|
|
2726
3524
|
const root = txn.root ?? txn;
|
|
2727
3525
|
if (!txn.isChainCommitting() || !root.commitSubmitted)
|
|
2728
3526
|
return false;
|
|
@@ -2735,27 +3533,36 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
|
|
|
2735
3533
|
// diagnostic grace, nor report a legitimately slow one early.
|
|
2736
3534
|
const now = performance.now();
|
|
2737
3535
|
const deadline = (root.deferredPoisonDeadline ??= now + MAX_DEFERRED_POISON_TICKS * expiration);
|
|
3536
|
+
// A handle with a durable log entry retrying a conflict has a known outcome — it commits once the
|
|
3537
|
+
// conflicting intent clears — so while its retry counter keeps advancing it is neither stalled nor
|
|
3538
|
+
// poisoned, and noteOverdueLoggedCommit is the operator's signal. A counter that has not moved for
|
|
3539
|
+
// LOGGED_RETRY_STALL_MS is a native attempt that never settled, which is the stall handled below.
|
|
3540
|
+
const loggedRetries = chainDurableLogRetries(root, txn);
|
|
3541
|
+
let loggedRetry = false;
|
|
3542
|
+
if (loggedRetries >= 0) {
|
|
3543
|
+
if (root.loggedRetriesTick === tick) {
|
|
3544
|
+
loggedRetry = Boolean(root.loggedRetryAdvancing);
|
|
3545
|
+
}
|
|
3546
|
+
else {
|
|
3547
|
+
if (root.loggedRetriesSeen !== loggedRetries) {
|
|
3548
|
+
root.loggedRetriesSeen = loggedRetries;
|
|
3549
|
+
root.loggedRetriesChangedAt = now;
|
|
3550
|
+
}
|
|
3551
|
+
loggedRetry = now - root.loggedRetriesChangedAt < LOGGED_RETRY_STALL_MS;
|
|
3552
|
+
root.loggedRetriesTick = tick;
|
|
3553
|
+
root.loggedRetryAdvancing = loggedRetry;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
2738
3556
|
let hasUnsubmittedCommit = false;
|
|
2739
3557
|
let unsubmittedCommitGraceExpired = false;
|
|
2740
3558
|
if (now >= deadline) {
|
|
2741
3559
|
// Only reclaim after the commit has genuinely stalled. Submitted writes no longer depend on these
|
|
2742
3560
|
// separate snapshots; pre-submit handles can still carry writes needed by the attempt.
|
|
2743
3561
|
txn.releaseStalledCommitReadResources();
|
|
2744
|
-
if (!txn.isProtectedCommit()) {
|
|
3562
|
+
if (!loggedRetry && !txn.isProtectedCommit()) {
|
|
2745
3563
|
if (!root.postSubmitPoisoned)
|
|
2746
3564
|
txn.poisonAfterStalledSubmittedCommit();
|
|
2747
|
-
const
|
|
2748
|
-
const collect = (start) => {
|
|
2749
|
-
for (let link = start; link; link = link.next)
|
|
2750
|
-
links.add(link);
|
|
2751
|
-
};
|
|
2752
|
-
collect(root);
|
|
2753
|
-
collect(txn);
|
|
2754
|
-
collect(root.submittedLink);
|
|
2755
|
-
if (root.submittedLinks)
|
|
2756
|
-
for (const link of root.submittedLinks)
|
|
2757
|
-
collect(link);
|
|
2758
|
-
for (const link of links) {
|
|
3565
|
+
for (const link of collectCommitLinks(root, txn)) {
|
|
2759
3566
|
if (link.committing && !link.nativeCommitSubmitted) {
|
|
2760
3567
|
hasUnsubmittedCommit = true;
|
|
2761
3568
|
break;
|
|
@@ -2775,14 +3582,16 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
|
|
|
2775
3582
|
txn.open === exports.TRANSACTION_STATE.CLOSED &&
|
|
2776
3583
|
!(txn === root && submittedCommitStillInFlight(root)))
|
|
2777
3584
|
return hasUnsubmittedCommit && !unsubmittedCommitGraceExpired;
|
|
2778
|
-
if (!
|
|
3585
|
+
if (!loggedRetry &&
|
|
3586
|
+
!root.stalledCommitLogged &&
|
|
3587
|
+
now - lastStalledCommitErrorAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
|
|
2779
3588
|
lastStalledCommitErrorAt = now;
|
|
2780
3589
|
root.stalledCommitLogged = true;
|
|
2781
3590
|
harperLogger.error(`A native commit has not settled after exceeding the open-transaction limit by a further ${MAX_DEFERRED_POISON_TICKS * expiration}ms. Its outcome is unknown, so Harper will not abort or delete its writes; fresh ordinary work ` +
|
|
2782
3591
|
`on this transaction is rejected until it settles, and the worker may require a restart, from table: ${txn.db?.name + (url ? ' path: ' + url : '')}`);
|
|
2783
3592
|
}
|
|
2784
3593
|
}
|
|
2785
|
-
else if (now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
|
|
3594
|
+
else if (!loggedRetry && now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
|
|
2786
3595
|
lastStalledCommitWarningAt = now;
|
|
2787
3596
|
harperLogger.warn?.(`Transaction exceeded the open-transaction limit after submitting a native commit; preserving its unknown outcome, from table: ${txn.db?.name + (url ? ' path: ' + url : '')}`);
|
|
2788
3597
|
}
|
|
@@ -2933,10 +3742,20 @@ function setMaxOutstandingTxnDuration(ms) {
|
|
|
2933
3742
|
MAX_OUTSTANDING_TXN_DURATION = ms;
|
|
2934
3743
|
return previous;
|
|
2935
3744
|
}
|
|
3745
|
+
/** Test seam: overrides the per-retry backoff cap and returns the previous value. */
|
|
3746
|
+
function setMaxRetryDelayMs(ms) {
|
|
3747
|
+
const previous = MAX_RETRY_DELAY_MS;
|
|
3748
|
+
MAX_RETRY_DELAY_MS = ms;
|
|
3749
|
+
return previous;
|
|
3750
|
+
}
|
|
2936
3751
|
/** Test seam: whether the monitor supervises this logical transaction for its writes. */
|
|
2937
3752
|
function isWriteSupervised(txn) {
|
|
2938
3753
|
return supervisedWriteRoots.has(txn);
|
|
2939
3754
|
}
|
|
3755
|
+
/** Test seam: returns the current open-transaction monitor interval. */
|
|
3756
|
+
function getTxnExpiration() {
|
|
3757
|
+
return txnExpiration;
|
|
3758
|
+
}
|
|
2940
3759
|
function setTxnExpiration(ms) {
|
|
2941
3760
|
clearInterval(timer);
|
|
2942
3761
|
txnExpiration = ms;
|