@harperfast/harper 5.3.2 → 5.3.3
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 +75 -10
- package/dist/resources/DatabaseTransaction.js +1080 -277
- package/dist/resources/DatabaseTransaction.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 +16 -2
- 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 +20 -17
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/mqtt.js +41 -23
- 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/utility/npmUtilities.js +18 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/package.json +1 -1
- package/resources/DESIGN.md +42 -10
- package/resources/DatabaseTransaction.ts +1244 -275
- package/resources/Resource.ts +39 -14
- package/resources/RocksTransactionLogStore.ts +1 -0
- package/resources/Table.ts +24 -2
- 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 +2 -0
- package/server/DurableSubscriptionsSession.ts +18 -16
- package/server/mqtt.ts +39 -22
- package/server/serverHelpers/NodeAdapterResponse.ts +19 -1
- package/studio/web/assets/Chat-B-Rd-ft9.js +2100 -0
- package/studio/web/assets/FloatingChat-CFypO7kh.js +23 -0
- package/studio/web/assets/SearchDialog-BRIyPiBl.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-BraYnzvC.js} +1 -1
- package/studio/web/assets/applications-XCds8-ZP.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-BY1w91l8.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-D5mpVs1b.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-DiXKiahz.js} +1 -1
- package/studio/web/assets/{htmlMode-CrL3YO5Y.js → htmlMode-DitVJ707.js} +1 -1
- package/studio/web/assets/index-CDnn0-PN.css +1 -0
- package/studio/web/assets/index-D8kyF6dt.js +833 -0
- package/studio/web/assets/index.lazy-DgbjUxLo.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-BBVXeMDm.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-D85_j2oD.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-U3mWhpRL.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-CH_6SYiA.js → lspLanguageFeatures-CoymyM3c.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-CPgEC6ZP.js +1 -0
- package/studio/web/assets/notifications-D_VqhHnx.js +1 -0
- package/studio/web/assets/operations-D8aaKYty.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-DM5E6Xls.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-WFvXqryO.js +1 -0
- package/studio/web/assets/{register-DCN4QUii.js → register-B7DvfAGj.js} +3 -3
- package/studio/web/assets/request-ChoMERRk.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-BHk22Rh6.js +1 -0
- package/studio/web/assets/setup-wNdpTrVH.js +2 -0
- package/studio/web/assets/{stateDiagram-v2-GCMORJYK-Bs8RIb9K.js → stateDiagram-v2-TBUQTH76-CnWE5yFn.js} +2 -2
- package/studio/web/assets/status-CYoonm9V.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-CKEm-P4K.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-Dv35qHmn.js} +1 -1
- package/studio/web/assets/{typescript-UMJSfZyF.js → typescript-Bly1EW8o.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-B379CS-h.js → useEntityRestURL-Cg3P0MOb.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-DVtjrvUa.js} +1 -1
- package/studio/web/assets/{xml-CG2N-1r1.js → xml-DjAPbgLN.js} +1 -1
- package/studio/web/assets/xychartDiagram-TAQBALBS-D5NV4Env.js +7 -0
- package/studio/web/assets/{yaml-68pda6Av.js → yaml-oTUyoJy1.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
|
|
@@ -851,7 +888,7 @@ class DatabaseTransaction {
|
|
|
851
888
|
*/
|
|
852
889
|
releaseReadTxn() {
|
|
853
890
|
const transaction = this.detachOwnedTransaction();
|
|
854
|
-
abortNativeTransaction(transaction, 'releasing timed-out read transaction');
|
|
891
|
+
abortNativeTransaction(transaction, 'releasing timed-out read transaction', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
855
892
|
this.completeDeferredContextRelease();
|
|
856
893
|
}
|
|
857
894
|
/**
|
|
@@ -904,6 +941,8 @@ class DatabaseTransaction {
|
|
|
904
941
|
if (index > -1)
|
|
905
942
|
this.writes[index] = null;
|
|
906
943
|
this.ownedWrites?.delete(operation);
|
|
944
|
+
if (operation.stagedIn === this)
|
|
945
|
+
operation.stagedIn = undefined;
|
|
907
946
|
if (operation.key === undefined)
|
|
908
947
|
return;
|
|
909
948
|
const writesForStore = this.writesByKey?.get(operation.store);
|
|
@@ -991,12 +1030,299 @@ class DatabaseTransaction {
|
|
|
991
1030
|
if (recordLocks.size === 0)
|
|
992
1031
|
this.recordLocks = undefined;
|
|
993
1032
|
}
|
|
994
|
-
|
|
1033
|
+
budgetCarrier(transaction) {
|
|
1034
|
+
const txn = transaction;
|
|
1035
|
+
if (txn?.commitStartedAt != null)
|
|
1036
|
+
return txn;
|
|
1037
|
+
if (this.saveCommits && txn && this.hasSiblingCommitInFlight(transaction ?? undefined)) {
|
|
1038
|
+
return txn;
|
|
1039
|
+
}
|
|
1040
|
+
if (txn?.immediateCommit)
|
|
1041
|
+
return txn;
|
|
1042
|
+
return this.saveCommits ? this : (this.root ?? this);
|
|
1043
|
+
}
|
|
1044
|
+
backfillActiveHandleWrites(handle) {
|
|
1045
|
+
if (handle.immediateCommit)
|
|
1046
|
+
return;
|
|
1047
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1048
|
+
const w = this.writes[i];
|
|
1049
|
+
if (w?.saved && !w.stagedOn)
|
|
1050
|
+
w.stagedOn = handle;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
isSingleHandleTarget(txn) {
|
|
1054
|
+
return (!txn.immediateCommit &&
|
|
1055
|
+
(!this.activeCommitHandles || this.activeCommitHandles.size === 0) &&
|
|
1056
|
+
(!this.activeCommitHandle || this.activeCommitHandle === txn));
|
|
1057
|
+
}
|
|
1058
|
+
getHandleStagedTxnTime(txn) {
|
|
1059
|
+
if (!txn)
|
|
1060
|
+
return undefined;
|
|
1061
|
+
if (this.isSingleHandleTarget(txn) &&
|
|
1062
|
+
(this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
|
|
1063
|
+
return this.activeHandleStagedTxnTime || txn.stagedTxnTime;
|
|
1064
|
+
}
|
|
1065
|
+
return txn.stagedTxnTime;
|
|
1066
|
+
}
|
|
1067
|
+
setHandleStagedTxnTime(txn, txnTime) {
|
|
1068
|
+
if (this.isSingleHandleTarget(txn) &&
|
|
1069
|
+
(this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
|
|
1070
|
+
if (!this.activeHandleStagedTxnTime)
|
|
1071
|
+
this.activeHandleStagedTxnTime = txnTime;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
txn.stagedTxnTime ??= txnTime;
|
|
1075
|
+
}
|
|
1076
|
+
isHandleCommitInFlight(txn) {
|
|
1077
|
+
if (!txn)
|
|
1078
|
+
return false;
|
|
1079
|
+
if (this.activeCommitHandle === txn) {
|
|
1080
|
+
return Boolean(this.activeHandleInFlight || txn.commitInFlight);
|
|
1081
|
+
}
|
|
1082
|
+
return Boolean(txn.commitInFlight);
|
|
1083
|
+
}
|
|
1084
|
+
setHandleCommitInFlight(txn, inFlight) {
|
|
1085
|
+
if (this.isSingleHandleTarget(txn) && inFlight && !this.activeCommitHandle) {
|
|
1086
|
+
this.activeCommitHandle = txn;
|
|
1087
|
+
}
|
|
1088
|
+
if (this.activeCommitHandle === txn) {
|
|
1089
|
+
this.activeHandleInFlight = inFlight;
|
|
1090
|
+
if (!inFlight) {
|
|
1091
|
+
txn.commitInFlight = false;
|
|
1092
|
+
}
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
txn.commitInFlight = inFlight;
|
|
1096
|
+
}
|
|
1097
|
+
isHandleCommitted(txn) {
|
|
1098
|
+
if (!txn)
|
|
1099
|
+
return false;
|
|
1100
|
+
if (this.activeCommitHandle === txn) {
|
|
1101
|
+
return Boolean(this.activeHandleCommitted || txn.committed);
|
|
1102
|
+
}
|
|
1103
|
+
return Boolean(txn.committed);
|
|
1104
|
+
}
|
|
1105
|
+
setHandleCommitted(txn) {
|
|
1106
|
+
if (this.activeCommitHandle === txn) {
|
|
1107
|
+
this.activeHandleInFlight = false;
|
|
1108
|
+
this.activeHandleCommitted = true;
|
|
1109
|
+
txn.commitInFlight = false;
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
txn.commitInFlight = false;
|
|
1113
|
+
txn.committed = true;
|
|
1114
|
+
}
|
|
1115
|
+
trackActiveCommitHandle(transaction) {
|
|
1116
|
+
if (!transaction.immediateCommit &&
|
|
1117
|
+
!this.activeCommitHandle &&
|
|
1118
|
+
(!this.activeCommitHandles || this.activeCommitHandles.size === 0)) {
|
|
1119
|
+
this.activeCommitHandle = transaction;
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (this.activeCommitHandle === transaction)
|
|
1123
|
+
return;
|
|
1124
|
+
let handles = this.activeCommitHandles;
|
|
1125
|
+
if (!handles) {
|
|
1126
|
+
handles = this.activeCommitHandles = new Set();
|
|
1127
|
+
}
|
|
1128
|
+
if (this.activeCommitHandle) {
|
|
1129
|
+
const first = this.activeCommitHandle;
|
|
1130
|
+
if (this.activeHandleStagedTxnTime && !first.stagedTxnTime) {
|
|
1131
|
+
first.stagedTxnTime = this.activeHandleStagedTxnTime;
|
|
1132
|
+
}
|
|
1133
|
+
if (this.activeHandleInFlight)
|
|
1134
|
+
first.commitInFlight = true;
|
|
1135
|
+
if (this.activeHandleCommitted)
|
|
1136
|
+
first.committed = true;
|
|
1137
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1138
|
+
this.activeHandleInFlight = false;
|
|
1139
|
+
this.activeHandleCommitted = false;
|
|
1140
|
+
this.backfillActiveHandleWrites(first);
|
|
1141
|
+
if (this.saveCommits) {
|
|
1142
|
+
if (this.commitStartedAt != null && first.commitStartedAt == null) {
|
|
1143
|
+
first.commitStartedAt = this.commitStartedAt;
|
|
1144
|
+
this.commitStartedAt = undefined;
|
|
1145
|
+
}
|
|
1146
|
+
if (this.commitOverdueLogged) {
|
|
1147
|
+
first.commitOverdueLogged = true;
|
|
1148
|
+
this.commitOverdueLogged = undefined;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
handles.add(this.activeCommitHandle);
|
|
1152
|
+
this.activeCommitHandle = undefined;
|
|
1153
|
+
}
|
|
1154
|
+
handles.add(transaction);
|
|
1155
|
+
}
|
|
1156
|
+
releaseActiveCommitHandle(transaction) {
|
|
1157
|
+
if (this.activeCommitHandle === transaction) {
|
|
1158
|
+
this.activeCommitHandle = undefined;
|
|
1159
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1160
|
+
this.activeHandleInFlight = false;
|
|
1161
|
+
this.activeHandleCommitted = false;
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const handles = this.activeCommitHandles;
|
|
1165
|
+
if (!handles)
|
|
1166
|
+
return;
|
|
1167
|
+
handles.delete(transaction);
|
|
1168
|
+
if (handles.size === 1) {
|
|
1169
|
+
for (const remaining of handles) {
|
|
1170
|
+
const rem = remaining;
|
|
1171
|
+
if (!rem.immediateCommit) {
|
|
1172
|
+
this.activeCommitHandle = remaining;
|
|
1173
|
+
this.activeHandleStagedTxnTime = rem.stagedTxnTime ?? 0;
|
|
1174
|
+
this.activeHandleInFlight = Boolean(rem.commitInFlight);
|
|
1175
|
+
this.activeHandleCommitted = Boolean(rem.committed);
|
|
1176
|
+
handles.clear();
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
clearActiveCommitHandles() {
|
|
1182
|
+
if (this.activeCommitHandle !== undefined || this.activeHandleStagedTxnTime) {
|
|
1183
|
+
this.activeCommitHandle = undefined;
|
|
1184
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1185
|
+
this.activeHandleInFlight = false;
|
|
1186
|
+
this.activeHandleCommitted = false;
|
|
1187
|
+
}
|
|
1188
|
+
this.activeCommitHandles?.clear();
|
|
1189
|
+
}
|
|
1190
|
+
hasActiveCommitHandle() {
|
|
1191
|
+
return (this.activeCommitHandle !== undefined ||
|
|
1192
|
+
(this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0));
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Retries so far of the handles with a durable transaction-log entry still retrying their commit on
|
|
1196
|
+
* this link, summed over overlapping commits so the total moves whenever any of them does, or -1
|
|
1197
|
+
* when there is none.
|
|
1198
|
+
*/
|
|
1199
|
+
activeDurableLogRetries() {
|
|
1200
|
+
let retries = -1;
|
|
1201
|
+
const note = (handle) => {
|
|
1202
|
+
const txn = handle;
|
|
1203
|
+
if (!txn || !hasDurableLog(txn) || this.isHandleCommitted(txn) || txn.abortedByWrapper)
|
|
1204
|
+
return;
|
|
1205
|
+
retries = Math.max(retries, 0) + (txn.retries ?? 0);
|
|
1206
|
+
};
|
|
1207
|
+
note(this.activeCommitHandle);
|
|
1208
|
+
if (this.activeCommitHandles)
|
|
1209
|
+
for (const handle of this.activeCommitHandles)
|
|
1210
|
+
note(handle);
|
|
1211
|
+
note(this.transaction);
|
|
1212
|
+
return retries;
|
|
1213
|
+
}
|
|
1214
|
+
isActiveCommitHandle(transaction) {
|
|
1215
|
+
return this.activeCommitHandle === transaction || Boolean(this.activeCommitHandles?.has(transaction));
|
|
1216
|
+
}
|
|
1217
|
+
hasSiblingCommitInFlight(targetTransaction) {
|
|
1218
|
+
const excluded = targetTransaction ?? this.transaction;
|
|
1219
|
+
if (this.activeCommitHandle)
|
|
1220
|
+
return this.activeCommitHandle !== excluded;
|
|
1221
|
+
const active = this.activeCommitHandles;
|
|
1222
|
+
if (!active || active.size === 0)
|
|
1223
|
+
return false;
|
|
1224
|
+
if (!excluded)
|
|
1225
|
+
return true;
|
|
1226
|
+
return active.size > (active.has(excluded) ? 1 : 0);
|
|
1227
|
+
}
|
|
1228
|
+
isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
|
|
1229
|
+
if (!write)
|
|
1230
|
+
return false;
|
|
1231
|
+
if (targetTransaction?.immediateCommit) {
|
|
1232
|
+
return write.stagedOn !== targetTransaction;
|
|
1233
|
+
}
|
|
1234
|
+
if (write.deferSave && !write.saved && this.saveCommits)
|
|
1235
|
+
return ((!fromAbort || Boolean(targetTransaction) || Boolean(this.isCommitting)) &&
|
|
1236
|
+
!write.stagedOn &&
|
|
1237
|
+
!write.lockHandle?.released &&
|
|
1238
|
+
!write.lockHandle?.isExpired());
|
|
1239
|
+
if (!write.saved)
|
|
1240
|
+
return !fromAbort && Boolean(targetTransaction) && this.open === exports.TRANSACTION_STATE.OPEN && !write.stagedOn;
|
|
1241
|
+
const stagedOn = (write.stagedOn ?? this.activeCommitHandle ?? this.transaction);
|
|
1242
|
+
if (!stagedOn || stagedOn === targetTransaction || stagedOn.abortedByWrapper || this.isHandleCommitted(stagedOn))
|
|
1243
|
+
return false;
|
|
1244
|
+
if (excludeImmediate && stagedOn.immediateCommit)
|
|
1245
|
+
return false;
|
|
1246
|
+
if (!fromAbort &&
|
|
1247
|
+
Boolean(targetTransaction) &&
|
|
1248
|
+
this.open === exports.TRANSACTION_STATE.OPEN &&
|
|
1249
|
+
stagedOn === this.transaction)
|
|
1250
|
+
return true;
|
|
1251
|
+
if (hasSiblingInFlight &&
|
|
1252
|
+
(Boolean(targetTransaction) ||
|
|
1253
|
+
(!fromAbort && this.isActiveCommitHandle(stagedOn)) ||
|
|
1254
|
+
(fromAbort &&
|
|
1255
|
+
this.isActiveCommitHandle(stagedOn) &&
|
|
1256
|
+
((!excludeImmediate && Boolean(stagedOn.immediateCommit)) || Boolean(stagedOn.isRetry)))))
|
|
1257
|
+
return true;
|
|
1258
|
+
return Boolean(this.isHandleCommitInFlight(stagedOn) || hasDurableLog(stagedOn));
|
|
1259
|
+
}
|
|
1260
|
+
hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
|
|
1261
|
+
if (!this.stagesWriteOnSave)
|
|
1262
|
+
return false;
|
|
1263
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1264
|
+
if (this.isWriteOwnedByActiveCommit(this.writes[i], targetTransaction, hasSiblingInFlight, fromAbort, excludeImmediate))
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
hasSavedWrite() {
|
|
1270
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1271
|
+
if (this.writes[i]?.saved)
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
hasSavedWriteForHandle(transaction, isImmediateCommit) {
|
|
1277
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1278
|
+
const write = this.writes[i];
|
|
1279
|
+
if (!write?.saved)
|
|
1280
|
+
continue;
|
|
1281
|
+
if (isImmediateCommit ? write.stagedOn === transaction : !write.stagedOn || write.stagedOn === transaction) {
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
retainTransactionLockHolds(transaction) {
|
|
1288
|
+
if (!transaction || !this.hasLeaseProtectedWrite)
|
|
1289
|
+
return;
|
|
1290
|
+
const txn = transaction;
|
|
1291
|
+
if (txn.lockHolds || txn.holdBudgetSpent)
|
|
1292
|
+
return;
|
|
1293
|
+
const carrier = this.budgetCarrier(transaction);
|
|
1294
|
+
if (carrier.commitStartedAt == null)
|
|
1295
|
+
carrier.commitStartedAt = performance.now();
|
|
1296
|
+
const budget = this.commitConflictBudget();
|
|
1297
|
+
const holdDeadlineMono = Number.isFinite(budget) ? carrier.commitStartedAt + budget : undefined;
|
|
1298
|
+
let holds;
|
|
1299
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
1300
|
+
const write = this.writes[i];
|
|
1301
|
+
if (!write?.saved || (write.stagedOn && write.stagedOn !== transaction))
|
|
1302
|
+
continue;
|
|
1303
|
+
const handle = write.lockHandle;
|
|
1304
|
+
if (!handle?.retainForCommit || (holds && holds.has(handle)))
|
|
1305
|
+
continue;
|
|
1306
|
+
handle.retainForCommit(holdDeadlineMono);
|
|
1307
|
+
(holds ??= new Set()).add(handle);
|
|
1308
|
+
}
|
|
1309
|
+
if (holds)
|
|
1310
|
+
txn.lockHolds = holds;
|
|
1311
|
+
}
|
|
1312
|
+
noteCommittedLockVersions(committedTransaction) {
|
|
995
1313
|
const recordLocks = this.recordLocks;
|
|
996
1314
|
if (!recordLocks)
|
|
997
1315
|
return;
|
|
1316
|
+
const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
|
|
1317
|
+
const filterByStagedOn = Boolean(committedTransaction || this.hasSiblingCommitInFlight(committedTransaction));
|
|
998
1318
|
for (const write of this.writes) {
|
|
999
|
-
if (write?.
|
|
1319
|
+
if (!write?.saved)
|
|
1320
|
+
continue;
|
|
1321
|
+
if (isImmediateCommit
|
|
1322
|
+
? write.stagedOn !== committedTransaction
|
|
1323
|
+
: filterByStagedOn && write.stagedOn && write.stagedOn !== committedTransaction)
|
|
1324
|
+
continue;
|
|
1325
|
+
if (write.appliedRecordVersion == null)
|
|
1000
1326
|
continue;
|
|
1001
1327
|
const handle = write.lockHandle ?? recordLocks.get(write.store)?.get(writeKeyId(write.key));
|
|
1002
1328
|
if (handle && !handle.released)
|
|
@@ -1005,9 +1331,32 @@ class DatabaseTransaction {
|
|
|
1005
1331
|
}
|
|
1006
1332
|
/**
|
|
1007
1333
|
* 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.
|
|
1334
|
+
* reused transaction never bases a write on a previous batch's staged state. When concurrent
|
|
1335
|
+
* commits share one DatabaseTransaction, only the writes staged on `committedTransaction` are
|
|
1336
|
+
* removed while another native commit is still in flight.
|
|
1009
1337
|
*/
|
|
1010
|
-
clearWrites() {
|
|
1338
|
+
clearWrites(committedTransaction, fromAbort = false) {
|
|
1339
|
+
if (committedTransaction)
|
|
1340
|
+
this.releaseActiveCommitHandle(committedTransaction);
|
|
1341
|
+
const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
|
|
1342
|
+
const hasSiblingInFlight = this.hasSiblingCommitInFlight(committedTransaction);
|
|
1343
|
+
if (this.stagesWriteOnSave &&
|
|
1344
|
+
(isImmediateCommit ||
|
|
1345
|
+
((hasSiblingInFlight || !committedTransaction || this.commitsInFlight > 1 || this.saveCommits) &&
|
|
1346
|
+
this.hasWriteOwnedByActiveCommit(committedTransaction, hasSiblingInFlight, fromAbort)))) {
|
|
1347
|
+
for (const write of [...this.writes]) {
|
|
1348
|
+
if (!write || this.isWriteOwnedByActiveCommit(write, committedTransaction, hasSiblingInFlight, fromAbort))
|
|
1349
|
+
continue;
|
|
1350
|
+
this.detachWrite(write);
|
|
1351
|
+
}
|
|
1352
|
+
this.writes = this.writes.filter((write) => write);
|
|
1353
|
+
this.validated = this.writes.reduce((acc, w, idx) => (w?.saved ? idx + 1 : acc), 0);
|
|
1354
|
+
if (this.hasLeaseProtectedWrite && !this.writes.some((write) => write?.lockHandle)) {
|
|
1355
|
+
this.hasLeaseProtectedWrite = false;
|
|
1356
|
+
}
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
this.clearActiveCommitHandles();
|
|
1011
1360
|
// A deferred write's `stagedIn` must not outlive this transaction's ability to commit it: save() can
|
|
1012
1361
|
// fire after a commit or abort, and routing it back here would revive a write this transaction
|
|
1013
1362
|
// already rolled back — whose blobs abort() has reclaimed. Cleared here, save() resolves the
|
|
@@ -1019,6 +1368,7 @@ class DatabaseTransaction {
|
|
|
1019
1368
|
this.ownedWrites?.delete(write);
|
|
1020
1369
|
}
|
|
1021
1370
|
this.writes = [];
|
|
1371
|
+
this.validated = 0;
|
|
1022
1372
|
this.writesByKey = undefined;
|
|
1023
1373
|
// The commit-fence latch belongs to the discarded batch. A reused transaction that once staged
|
|
1024
1374
|
// a locked write would otherwise re-scan every write of every later unlocked batch. Assigned
|
|
@@ -1098,12 +1448,13 @@ class DatabaseTransaction {
|
|
|
1098
1448
|
* no record), and writes with no stored entry at all — a delete leaves a readable tombstone on any
|
|
1099
1449
|
* audited or delete-tracking table, so only a delete on a table with neither is entry-less. Such a
|
|
1100
1450
|
* `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).
|
|
1451
|
+
* transaction is stamped with one version). `savedOnly` also skips a deferred write not yet saved,
|
|
1452
|
+
* which a commit leaves in place for a later one.
|
|
1102
1453
|
*/
|
|
1103
|
-
lastConfirmableEntry() {
|
|
1454
|
+
lastConfirmableEntry(savedOnly = false) {
|
|
1104
1455
|
for (let i = this.writes.length - 1; i >= 0; i--) {
|
|
1105
1456
|
const write = this.writes[i];
|
|
1106
|
-
if (!write || write.skipReplicationConfirmation)
|
|
1457
|
+
if (!write || write.skipReplicationConfirmation || (savedOnly && !write.saved))
|
|
1107
1458
|
continue;
|
|
1108
1459
|
const entry = write.store.getEntry(write.key);
|
|
1109
1460
|
if (entry)
|
|
@@ -1116,9 +1467,61 @@ class DatabaseTransaction {
|
|
|
1116
1467
|
* rejection in that window is an unhandled rejection (fatal under --unhandled-rejections=strict).
|
|
1117
1468
|
* The rejection still surfaces through commit()'s Promise.all.
|
|
1118
1469
|
*/
|
|
1119
|
-
stageCompletion(completion) {
|
|
1470
|
+
stageCompletion(completion, transaction) {
|
|
1120
1471
|
completion.then(undefined, () => { });
|
|
1472
|
+
const index = this.completions.length;
|
|
1121
1473
|
this.completions.push(completion);
|
|
1474
|
+
if (transaction &&
|
|
1475
|
+
(this.saveCommits ||
|
|
1476
|
+
Boolean(transaction.immediateCommit) ||
|
|
1477
|
+
this.hasActiveCommitHandle() ||
|
|
1478
|
+
Boolean(this.completionHandles))) {
|
|
1479
|
+
(this.completionHandles ??= [])[index] = transaction;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
takeCompletions(transaction) {
|
|
1483
|
+
if (this.completions.length === 0)
|
|
1484
|
+
return NO_COMPLETIONS;
|
|
1485
|
+
const handles = this.completionHandles;
|
|
1486
|
+
const isImmediate = Boolean(transaction?.immediateCommit);
|
|
1487
|
+
if (!handles) {
|
|
1488
|
+
if (isImmediate)
|
|
1489
|
+
return NO_COMPLETIONS;
|
|
1490
|
+
const completions = this.completions;
|
|
1491
|
+
this.completions = [];
|
|
1492
|
+
return completions;
|
|
1493
|
+
}
|
|
1494
|
+
let allMatch = true;
|
|
1495
|
+
for (let i = 0; i < this.completions.length; i++) {
|
|
1496
|
+
const handle = handles[i];
|
|
1497
|
+
if (!(handle ? handle === transaction : !isImmediate)) {
|
|
1498
|
+
allMatch = false;
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
if (allMatch) {
|
|
1503
|
+
const completions = this.completions;
|
|
1504
|
+
this.completions = [];
|
|
1505
|
+
this.completionHandles = undefined;
|
|
1506
|
+
return completions;
|
|
1507
|
+
}
|
|
1508
|
+
const taken = [];
|
|
1509
|
+
const remaining = [];
|
|
1510
|
+
const remainingHandles = [];
|
|
1511
|
+
for (let i = 0; i < this.completions.length; i++) {
|
|
1512
|
+
const completion = this.completions[i];
|
|
1513
|
+
const handle = handles[i];
|
|
1514
|
+
if (handle ? handle === transaction : !isImmediate) {
|
|
1515
|
+
taken.push(completion);
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
remaining.push(completion);
|
|
1519
|
+
remainingHandles.push(handle);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
this.completions = remaining;
|
|
1523
|
+
this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
|
|
1524
|
+
return taken;
|
|
1122
1525
|
}
|
|
1123
1526
|
/**
|
|
1124
1527
|
* Discard staged completions that no commit() will ever aggregate (abort path). Their rejections
|
|
@@ -1126,12 +1529,41 @@ class DatabaseTransaction {
|
|
|
1126
1529
|
* Clearing them also keeps a reused transaction's next commit() from rejecting with the previous
|
|
1127
1530
|
* batch's error.
|
|
1128
1531
|
*/
|
|
1129
|
-
drainCompletions() {
|
|
1532
|
+
drainCompletions(targetTransaction, preserveActiveWrites = false) {
|
|
1130
1533
|
if (this.completions.length === 0)
|
|
1131
1534
|
return;
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1535
|
+
const handles = this.completionHandles;
|
|
1536
|
+
const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
|
|
1537
|
+
let drained;
|
|
1538
|
+
if (!preserveActiveWrites) {
|
|
1539
|
+
drained = this.completions;
|
|
1540
|
+
this.completions = [];
|
|
1541
|
+
this.completionHandles = undefined;
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
drained = [];
|
|
1545
|
+
const remaining = [];
|
|
1546
|
+
const remainingHandles = [];
|
|
1547
|
+
for (let i = 0; i < this.completions.length; i++) {
|
|
1548
|
+
const completion = this.completions[i];
|
|
1549
|
+
const handle = handles?.[i];
|
|
1550
|
+
const keep = isImmediateTarget
|
|
1551
|
+
? handle !== targetTransaction
|
|
1552
|
+
: Boolean(handle) &&
|
|
1553
|
+
handle !== targetTransaction &&
|
|
1554
|
+
(this.isActiveCommitHandle(handle) || hasDurableLog(handle));
|
|
1555
|
+
if (keep) {
|
|
1556
|
+
remaining.push(completion);
|
|
1557
|
+
remainingHandles.push(handle);
|
|
1558
|
+
}
|
|
1559
|
+
else {
|
|
1560
|
+
drained.push(completion);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
this.completions = remaining;
|
|
1564
|
+
this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
|
|
1565
|
+
}
|
|
1566
|
+
for (const completion of drained)
|
|
1135
1567
|
completion.then(undefined, (error) => harperLogger.warn?.('A staged transaction completion failed after the transaction was aborted', error));
|
|
1136
1568
|
}
|
|
1137
1569
|
addWrite(operation) {
|
|
@@ -1205,7 +1637,7 @@ class DatabaseTransaction {
|
|
|
1205
1637
|
// (the same rule the pre-submit fence applies below). Without the distinction,
|
|
1206
1638
|
// `lock(); save(); unlock(); commit()` succeeded normally and threw 409 only when a conflict
|
|
1207
1639
|
// retry or an open read iterator forced the replay to re-save it.
|
|
1208
|
-
if (operation.saved ? lockHandle?.isLeaseExpired() : lockHandle?.isExpired()) {
|
|
1640
|
+
if (operation.saved ? lockHandle?.isLeaseExpired() && !hasDurableLog(transaction) : lockHandle?.isExpired()) {
|
|
1209
1641
|
// Through `detachWrite`, which also repairs the per-key chain: dropping it from `writes` alone
|
|
1210
1642
|
// leaves `writesByKey` pointing at a write that was refused, so a caller that catches this 409
|
|
1211
1643
|
// and stages the same key again would take the rejected operation as its merge basis.
|
|
@@ -1229,7 +1661,10 @@ class DatabaseTransaction {
|
|
|
1229
1661
|
operation.lockStamp = lockHandle.holderVersionCandidate();
|
|
1230
1662
|
}
|
|
1231
1663
|
}
|
|
1232
|
-
|
|
1664
|
+
const concurrentClosedWrite = Boolean(transaction?.immediateCommit) ||
|
|
1665
|
+
(!transaction && this.open !== exports.TRANSACTION_STATE.OPEN && !this.saveCommits);
|
|
1666
|
+
let txnTime = operation.lockStamp ?? this.getHandleStagedTxnTime(transaction) ?? (concurrentClosedWrite ? 0 : this.timestamp);
|
|
1667
|
+
const mintedTimestamp = !txnTime;
|
|
1233
1668
|
// Only an OPEN transaction accepts new staged writes. After commit, this.transaction may still
|
|
1234
1669
|
// be retained for outstanding read iterators; staging into it would silently discard the write
|
|
1235
1670
|
// when doneReadTxn() aborts the handle, so such writes commit immediately on a fresh
|
|
@@ -1260,20 +1695,43 @@ class DatabaseTransaction {
|
|
|
1260
1695
|
else {
|
|
1261
1696
|
// if it is closed, we have to immediately commit, using our immediate transaction
|
|
1262
1697
|
immediateCommit = true;
|
|
1698
|
+
if (!this.saveCommits)
|
|
1699
|
+
transaction.immediateCommit = true;
|
|
1263
1700
|
}
|
|
1264
1701
|
if (txnTime) {
|
|
1265
1702
|
transaction.setTimestamp(txnTime);
|
|
1266
1703
|
}
|
|
1267
1704
|
}
|
|
1705
|
+
if (this.activeCommitHandle && this.activeCommitHandle !== transaction) {
|
|
1706
|
+
this.backfillActiveHandleWrites(this.activeCommitHandle);
|
|
1707
|
+
}
|
|
1708
|
+
else if (!this.activeCommitHandle && this.transaction && this.transaction !== transaction) {
|
|
1709
|
+
this.backfillActiveHandleWrites(this.transaction);
|
|
1710
|
+
}
|
|
1711
|
+
const shouldStampStagedOn = this.saveCommits ||
|
|
1712
|
+
immediateCommit ||
|
|
1713
|
+
Boolean(transaction?.immediateCommit) ||
|
|
1714
|
+
operation.stagedOn !== undefined ||
|
|
1715
|
+
(this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0) ||
|
|
1716
|
+
(this.activeCommitHandle !== undefined && this.activeCommitHandle !== transaction) ||
|
|
1717
|
+
(this.transaction != null && this.transaction !== transaction);
|
|
1268
1718
|
if (this.isReplay) {
|
|
1269
1719
|
// Replayed writes came FROM the transaction log; never re-append them —
|
|
1270
1720
|
// replay iterates that same log, so re-appending prevents convergence
|
|
1271
1721
|
// (boot hangs replaying its own output). Conflict retries stamp isRetry
|
|
1272
1722
|
// at the retry sites in commit(); this is the replay-path equivalent.
|
|
1273
1723
|
transaction.isRetry = true;
|
|
1724
|
+
transaction.retries = this.retries;
|
|
1274
1725
|
}
|
|
1275
|
-
if (!txnTime)
|
|
1276
|
-
txnTime =
|
|
1726
|
+
if (!txnTime) {
|
|
1727
|
+
txnTime = transaction.getTimestamp();
|
|
1728
|
+
if (!concurrentClosedWrite)
|
|
1729
|
+
this.timestamp = txnTime;
|
|
1730
|
+
}
|
|
1731
|
+
if (!operation.lockStamp)
|
|
1732
|
+
this.setHandleStagedTxnTime(transaction, txnTime);
|
|
1733
|
+
if (this.saveCommits && mintedTimestamp)
|
|
1734
|
+
this.timestamp = 0;
|
|
1277
1735
|
if (!operation.saved && operation.pendingPriorWrite) {
|
|
1278
1736
|
const pendingWrites = [];
|
|
1279
1737
|
for (let pending = operation.pendingPriorWrite; pending;) {
|
|
@@ -1301,50 +1759,76 @@ class DatabaseTransaction {
|
|
|
1301
1759
|
// An entry read uncached through this same pinned-snapshot handle is already that base, until a retry resets
|
|
1302
1760
|
// the snapshot or an earlier staged write to the key changes what a read returns; the resource withholds the
|
|
1303
1761
|
// handle once its entry was replaced or evicted, and locked, snapshot-free and replayed writes always reload.
|
|
1762
|
+
const txnRetries = transaction?.retries ?? 0;
|
|
1304
1763
|
const reusesBaseRead = operation.baseReadTxn === transaction &&
|
|
1305
|
-
|
|
1764
|
+
txnRetries === 0 &&
|
|
1306
1765
|
!reloadEntry &&
|
|
1307
1766
|
!operation.priorWrite &&
|
|
1308
1767
|
!operation.lockHandle &&
|
|
1309
1768
|
!this.snapshotFree &&
|
|
1310
1769
|
!transaction.snapshotDisabled &&
|
|
1311
1770
|
!this.isReplay;
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
if (
|
|
1771
|
+
const wasSaved = Boolean(operation.saved);
|
|
1772
|
+
let completion;
|
|
1773
|
+
try {
|
|
1774
|
+
if (!reusesBaseRead && (reloadEntry || operation.entry === undefined || reloadsCommitBase)) {
|
|
1775
|
+
const uncachedRead = !!operation.reloadCommitBase || reloadEntry || this.isReplay;
|
|
1776
|
+
operation.entry = operation.store.getEntry(operation.key, { transaction, uncachedRead });
|
|
1777
|
+
}
|
|
1778
|
+
if (!operation.saved) {
|
|
1779
|
+
// immediately execute in this transaction
|
|
1780
|
+
const validated = validateWrite(operation, writeVersion, this);
|
|
1781
|
+
if (validated === false) {
|
|
1782
|
+
operation.saved = true;
|
|
1783
|
+
if (shouldStampStagedOn)
|
|
1784
|
+
operation.stagedOn = transaction;
|
|
1785
|
+
operation.commit = () => { }; // noop if we try again
|
|
1786
|
+
closeWriteInstance(operation);
|
|
1787
|
+
if (immediateCommit) {
|
|
1788
|
+
this.detachWrite(operation);
|
|
1789
|
+
if (!this.activeCommitHandle)
|
|
1790
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1791
|
+
abortNativeTransaction(transaction, 'aborting an immediate transaction with nothing to write', operation.store, this.startedFrom);
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1320
1795
|
operation.saved = true;
|
|
1321
|
-
|
|
1796
|
+
let result = operation.before?.();
|
|
1797
|
+
if (result?.then)
|
|
1798
|
+
this.stageCompletion(result, transaction);
|
|
1799
|
+
result = operation.beforeIntermediate?.();
|
|
1800
|
+
if (result?.then)
|
|
1801
|
+
this.stageCompletion(result, transaction);
|
|
1802
|
+
}
|
|
1803
|
+
if (shouldStampStagedOn)
|
|
1804
|
+
operation.stagedOn = transaction;
|
|
1805
|
+
if (lockHandle || this.recordLocks)
|
|
1806
|
+
operation.trackRecordVersion = true;
|
|
1807
|
+
if (operation.trackRecordVersion)
|
|
1808
|
+
operation.recordVersionApplied = false;
|
|
1809
|
+
try {
|
|
1810
|
+
completion = operation.commit(writeVersion, operation.entry, (wasSaved || this.isReplay) && txnRetries > 0, transaction, this);
|
|
1811
|
+
}
|
|
1812
|
+
finally {
|
|
1322
1813
|
closeWriteInstance(operation);
|
|
1323
|
-
return;
|
|
1324
1814
|
}
|
|
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
1815
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1816
|
+
catch (error) {
|
|
1817
|
+
// A handle created for an immediate commit is owned by nobody else: a throw before its commit
|
|
1818
|
+
// is submitted must release it rather than leak it.
|
|
1819
|
+
if (immediateCommit) {
|
|
1820
|
+
this.drainCompletions(transaction, true);
|
|
1821
|
+
this.detachWrite(operation);
|
|
1822
|
+
if (!this.activeCommitHandle)
|
|
1823
|
+
this.activeHandleStagedTxnTime = 0;
|
|
1824
|
+
abortNativeTransaction(transaction, 'aborting an immediate transaction whose write threw', operation.store, this.startedFrom);
|
|
1825
|
+
}
|
|
1826
|
+
throw error;
|
|
1343
1827
|
}
|
|
1344
1828
|
if (operation.trackRecordVersion)
|
|
1345
1829
|
operation.appliedRecordVersion = operation.recordVersionApplied ? writeVersion : undefined;
|
|
1346
1830
|
if (typeof completion?.then === 'function')
|
|
1347
|
-
this.stageCompletion(completion);
|
|
1831
|
+
this.stageCompletion(completion, transaction);
|
|
1348
1832
|
// Sticky record that THIS write staged with its audit entry appended (log entries batch on the
|
|
1349
1833
|
// native transaction and are durably written by its commit attempt — even a failed one — so
|
|
1350
1834
|
// they survive the abort-after-failed-commit of the retry paths). isRetry stagings
|
|
@@ -1353,6 +1837,7 @@ class DatabaseTransaction {
|
|
|
1353
1837
|
// retries where a recommit round self-skips before a fresh-transaction replay.
|
|
1354
1838
|
if (!operation.skipped && !transaction.isRetry) {
|
|
1355
1839
|
operation.appendedAuditEntry = true;
|
|
1840
|
+
operation.appendedTo = transaction;
|
|
1356
1841
|
}
|
|
1357
1842
|
if (immediateCommit) {
|
|
1358
1843
|
// immediately commit if the harper transaction is closed
|
|
@@ -1549,6 +2034,10 @@ class DatabaseTransaction {
|
|
|
1549
2034
|
endCommitAttempt() {
|
|
1550
2035
|
this.commitsInFlight--;
|
|
1551
2036
|
if (!this.commitsInFlight) {
|
|
2037
|
+
this.clearActiveCommitHandles();
|
|
2038
|
+
this.commitStartedAt = undefined;
|
|
2039
|
+
if (this.commitOverdueLogged)
|
|
2040
|
+
this.commitOverdueLogged = undefined;
|
|
1552
2041
|
this.nativeCommitSubmitted = false;
|
|
1553
2042
|
this.nativeCommitAttemptEnded();
|
|
1554
2043
|
}
|
|
@@ -1568,7 +2057,14 @@ class DatabaseTransaction {
|
|
|
1568
2057
|
root.submittedCommitSupervised = false;
|
|
1569
2058
|
root.deferredPoisonDeadline = undefined;
|
|
1570
2059
|
root.unsubmittedCommitDeadline = undefined;
|
|
2060
|
+
root.loggedRetriesSeen = undefined;
|
|
2061
|
+
root.loggedRetriesChangedAt = undefined;
|
|
2062
|
+
root.loggedRetriesTick = undefined;
|
|
2063
|
+
root.loggedRetryAdvancing = undefined;
|
|
1571
2064
|
root.stalledCommitLogged = false;
|
|
2065
|
+
root.commitStartedAt = undefined;
|
|
2066
|
+
if (root.commitOverdueLogged)
|
|
2067
|
+
root.commitOverdueLogged = undefined;
|
|
1572
2068
|
const submittedLink = root.submittedLink;
|
|
1573
2069
|
root.submittedLink = undefined;
|
|
1574
2070
|
const submittedLinks = root.submittedLinks;
|
|
@@ -1643,35 +2139,57 @@ class DatabaseTransaction {
|
|
|
1643
2139
|
// reused across retries — the native layer resets it in place (fresh snapshot) on IsBusy/TryAgain —
|
|
1644
2140
|
// but reassigned to a fresh replay transaction when outstanding read iterators retain this.transaction
|
|
1645
2141
|
let transaction = options.transaction ?? this.transaction;
|
|
2142
|
+
const retryRound = Boolean(options.transaction?.isRetry);
|
|
2143
|
+
const isImmediateCommit = Boolean(transaction?.immediateCommit);
|
|
1646
2144
|
try {
|
|
2145
|
+
if (retryRound && this.activeCommitHandle && this.activeCommitHandle !== transaction) {
|
|
2146
|
+
this.backfillActiveHandleWrites(this.activeCommitHandle);
|
|
2147
|
+
}
|
|
1647
2148
|
for (let i = 0; i < this.writes.length; i++) {
|
|
1648
2149
|
let operation = this.writes[i];
|
|
1649
|
-
if (!operation ||
|
|
2150
|
+
if (!operation ||
|
|
2151
|
+
(isImmediateCommit
|
|
2152
|
+
? !retryRound || !operation.saved || operation.stagedOn !== transaction
|
|
2153
|
+
: retryRound
|
|
2154
|
+
? !operation.saved || (Boolean(operation.stagedOn) && operation.stagedOn !== transaction)
|
|
2155
|
+
: Boolean(operation.saved) || (this.saveCommits && Boolean(operation.deferSave))))
|
|
1650
2156
|
continue;
|
|
1651
|
-
this.save(operation, transaction, i < this.validated, options);
|
|
2157
|
+
this.save(operation, transaction, Boolean(operation.saved) || i < this.validated, options);
|
|
2158
|
+
if (!transaction)
|
|
2159
|
+
transaction = this.transaction;
|
|
1652
2160
|
}
|
|
2161
|
+
if (transaction && (this.writes.length > 0 || hasDurableLog(transaction)))
|
|
2162
|
+
this.trackActiveCommitHandle(transaction);
|
|
1653
2163
|
}
|
|
1654
2164
|
catch (error) {
|
|
2165
|
+
if (!transaction)
|
|
2166
|
+
transaction = this.transaction ?? undefined;
|
|
1655
2167
|
// abort() releases only this.transaction; a retry round's handle was detached before its
|
|
1656
2168
|
// 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();
|
|
2169
|
+
if (transaction && transaction !== this.transaction)
|
|
2170
|
+
abortNativeTransaction(transaction, 'aborting a retry transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2171
|
+
this.abort(false, true, transaction);
|
|
1660
2172
|
throw error;
|
|
1661
2173
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2174
|
+
if (!isImmediateCommit) {
|
|
2175
|
+
this.validated = this.saveCommits
|
|
2176
|
+
? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
|
|
2177
|
+
: this.writes.length;
|
|
2178
|
+
}
|
|
2179
|
+
const completions = this.takeCompletions(transaction);
|
|
1666
2180
|
const stagedWrites = this.writes.length;
|
|
1667
|
-
|
|
2181
|
+
let inCommitPhase = false;
|
|
2182
|
+
if (completions.length > 0 && !isImmediateCommit) {
|
|
2183
|
+
inCommitPhase = true;
|
|
1668
2184
|
this.setCommitPhase(true);
|
|
1669
2185
|
}
|
|
1670
2186
|
let commitResult;
|
|
1671
2187
|
try {
|
|
1672
2188
|
commitResult = (0, when_ts_1.when)(completions.length > 0 ? Promise.all(completions) : null, () => {
|
|
1673
|
-
if (
|
|
2189
|
+
if (inCommitPhase) {
|
|
2190
|
+
inCommitPhase = false;
|
|
1674
2191
|
this.setCommitPhase(false);
|
|
2192
|
+
}
|
|
1675
2193
|
// The transaction can be aborted underneath us while we are parked in the await above — by the
|
|
1676
2194
|
// monitor once the commit phase outlives its grace, or through the multi-store poison chain.
|
|
1677
2195
|
// abort() cleared the write set and released the handle, so resuming would commit nothing and
|
|
@@ -1685,7 +2203,11 @@ class DatabaseTransaction {
|
|
|
1685
2203
|
}
|
|
1686
2204
|
if (stagedWrites > 0 && this.writes.length === 0 && this.open === exports.TRANSACTION_STATE.CLOSED)
|
|
1687
2205
|
throw new hdbError_ts_1.ServerError('Transaction was aborted while its commit was waiting on pre-commit work', 500);
|
|
1688
|
-
if (
|
|
2206
|
+
if (!retryRound &&
|
|
2207
|
+
!isImmediateCommit &&
|
|
2208
|
+
(this.saveCommits
|
|
2209
|
+
? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
|
|
2210
|
+
: this.writes.length) > this.validated) {
|
|
1689
2211
|
// 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
2212
|
return this.commit({ ...options, continuation: true });
|
|
1691
2213
|
}
|
|
@@ -1697,9 +2219,10 @@ class DatabaseTransaction {
|
|
|
1697
2219
|
// and replay branches below deliberately commit a handle other than this.transaction.
|
|
1698
2220
|
if (!transaction)
|
|
1699
2221
|
transaction = this.transaction;
|
|
1700
|
-
if (!options.transaction && this.
|
|
2222
|
+
if (!options.transaction && this.hasSavedWrite())
|
|
1701
2223
|
this.renewChainForNativeCommit();
|
|
1702
|
-
|
|
2224
|
+
if (!isImmediateCommit)
|
|
2225
|
+
this.open = exports.TRANSACTION_STATE.CLOSED;
|
|
1703
2226
|
// RocksTransaction.commit() resolves with RETRY_NOW_VALUE (a number) under
|
|
1704
2227
|
// coordinatedRetry, or void on a normal commit/abort.
|
|
1705
2228
|
let commitResolution;
|
|
@@ -1707,11 +2230,12 @@ class DatabaseTransaction {
|
|
|
1707
2230
|
// recursions, immediate-commit re-entries, and a second top-level commit() (wrapper
|
|
1708
2231
|
// commit after an explicit in-handler commit) must not steal a reference owned by an
|
|
1709
2232
|
// outstanding iterator (doneReadTxn() would then never release the native handle).
|
|
1710
|
-
if (!this.baseReadRefConsumed) {
|
|
2233
|
+
if (!isImmediateCommit && !this.baseReadRefConsumed) {
|
|
1711
2234
|
this.baseReadRefConsumed = true;
|
|
1712
2235
|
this.readTxnsUsed--;
|
|
1713
2236
|
}
|
|
1714
|
-
|
|
2237
|
+
let hadLeaseProtectedWrite = false;
|
|
2238
|
+
if (!isImmediateCommit && this.readTxnsUsed > 0) {
|
|
1715
2239
|
// Outstanding iterators still stream through this.transaction — their native iterators
|
|
1716
2240
|
// live inside it (GetIterator wraps its write batch + snapshot), so committing or
|
|
1717
2241
|
// aborting the handle now would invalidate them mid-stream. Leave it open for the
|
|
@@ -1723,7 +2247,7 @@ class DatabaseTransaction {
|
|
|
1723
2247
|
// kept the last iterator from finishing cleanly — a hung stream, the long-transaction
|
|
1724
2248
|
// monitor's timeout abort — dropped writes the caller had already been told committed.
|
|
1725
2249
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
1726
|
-
if (this.
|
|
2250
|
+
if (this.hasSavedWrite() || hasDurableLog(transaction)) {
|
|
1727
2251
|
if (!options.transaction) {
|
|
1728
2252
|
if (!replayedWritesWarned) {
|
|
1729
2253
|
replayedWritesWarned = true;
|
|
@@ -1741,24 +2265,40 @@ class DatabaseTransaction {
|
|
|
1741
2265
|
const replayTransaction = new rocksdb_js_1.Transaction((this.writes[0].store.store ?? this.db.store), { coordinatedRetry: true });
|
|
1742
2266
|
if (this.timestamp)
|
|
1743
2267
|
replayTransaction.setTimestamp(this.timestamp);
|
|
1744
|
-
|
|
2268
|
+
const retries = (transaction?.retries ?? this.retries) + 1;
|
|
2269
|
+
replayTransaction.retries = retries;
|
|
2270
|
+
this.retries = retries; // a replay round: commit handlers re-base on the reloaded entries
|
|
2271
|
+
const prevStagedTxnTime = this.getHandleStagedTxnTime(transaction);
|
|
2272
|
+
if (transaction)
|
|
2273
|
+
this.releaseActiveCommitHandle(transaction);
|
|
2274
|
+
this.trackActiveCommitHandle(replayTransaction);
|
|
2275
|
+
if (prevStagedTxnTime)
|
|
2276
|
+
this.setHandleStagedTxnTime(replayTransaction, prevStagedTxnTime);
|
|
2277
|
+
transaction = replayTransaction;
|
|
1745
2278
|
try {
|
|
1746
2279
|
for (const operation of this.writes) {
|
|
2280
|
+
if (!operation?.saved)
|
|
2281
|
+
continue;
|
|
1747
2282
|
this.save(operation, replayTransaction, true, options);
|
|
1748
2283
|
}
|
|
1749
2284
|
}
|
|
1750
2285
|
catch (error) {
|
|
1751
|
-
abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw');
|
|
1752
|
-
this.
|
|
2286
|
+
abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2287
|
+
this.releaseActiveCommitHandle(replayTransaction);
|
|
2288
|
+
this.abort(false, true, this.hasSiblingCommitInFlight(replayTransaction) ? replayTransaction : undefined);
|
|
1753
2289
|
throw error;
|
|
1754
2290
|
}
|
|
1755
|
-
transaction = replayTransaction;
|
|
1756
2291
|
}
|
|
1757
2292
|
// with options.transaction set this is a retry round — the save loop above already
|
|
1758
2293
|
// re-staged the writes into it
|
|
1759
2294
|
this.assertDatabaseCommitAllowed(transaction);
|
|
1760
2295
|
this.markNativeCommitSubmitted();
|
|
1761
2296
|
commitResolution = transaction.commit();
|
|
2297
|
+
this.setHandleCommitInFlight(transaction, true);
|
|
2298
|
+
if (this.hasLeaseProtectedWrite) {
|
|
2299
|
+
hadLeaseProtectedWrite = true;
|
|
2300
|
+
this.retainTransactionLockHolds(transaction);
|
|
2301
|
+
}
|
|
1762
2302
|
recordCommitLatency(commitResolution, performance.now());
|
|
1763
2303
|
// Write-queue-depth accounting for this replay commit happens uniformly below, via
|
|
1764
2304
|
// trackOutstandingCommit(commitResolution) — see that function's comment. Omitting
|
|
@@ -1779,10 +2319,11 @@ class DatabaseTransaction {
|
|
|
1779
2319
|
}
|
|
1780
2320
|
else {
|
|
1781
2321
|
// no more reads need to be performed, just commit/abort based if there are any writes
|
|
1782
|
-
|
|
2322
|
+
if (!isImmediateCommit)
|
|
2323
|
+
this.detachOwnedTransaction(); // any further operations operate immediately
|
|
1783
2324
|
if (transaction) {
|
|
1784
2325
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
1785
|
-
if (this.
|
|
2326
|
+
if (this.hasSavedWriteForHandle(transaction, isImmediateCommit) || hasDurableLog(transaction)) {
|
|
1786
2327
|
// Commit retries can construct fresh ranges on this live handle after read ownership ends.
|
|
1787
2328
|
readTransactionOwners.delete(transaction);
|
|
1788
2329
|
// Re-fence before submitting: the loop above skips operations already marked saved, so
|
|
@@ -1790,25 +2331,30 @@ class DatabaseTransaction {
|
|
|
1790
2331
|
// retry/replay path re-saves every operation and is fenced there instead. Lease expiry
|
|
1791
2332
|
// only: an unlock() inside the lease leaves the staged write valid, an elapsed lease
|
|
1792
2333
|
// 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
|
-
|
|
2334
|
+
if (this.hasLeaseProtectedWrite && !hasDurableLog(transaction)) {
|
|
2335
|
+
for (let i = 0; i < this.writes.length; i++) {
|
|
2336
|
+
const write = this.writes[i];
|
|
2337
|
+
if (!write?.saved ||
|
|
2338
|
+
(isImmediateCommit
|
|
2339
|
+
? write.stagedOn !== transaction
|
|
2340
|
+
: Boolean(write.stagedOn) && write.stagedOn !== transaction))
|
|
2341
|
+
continue;
|
|
2342
|
+
const lapsed = write.lockHandle;
|
|
2343
|
+
if (!lapsed?.isLeaseExpired())
|
|
2344
|
+
continue;
|
|
2345
|
+
abortNativeTransaction(transaction, 'aborting a transaction whose record lock lapsed before submission', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2346
|
+
// Every other terminal exit from commit() runs the logical cleanup too. Throwing
|
|
2347
|
+
// straight out would strand the OTHER locks this transaction holds, its staged
|
|
2348
|
+
// blobs, and the context's back-reference to a CLOSED transaction — and
|
|
2349
|
+
// transaction()'s onComplete has no rejection path to run it later.
|
|
2350
|
+
try {
|
|
2351
|
+
this.abort(false, true, transaction);
|
|
2352
|
+
}
|
|
2353
|
+
catch (error) {
|
|
2354
|
+
harperLogger.debug?.('cleaning up a transaction whose record lock lapsed', error);
|
|
2355
|
+
}
|
|
2356
|
+
throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
|
|
1810
2357
|
}
|
|
1811
|
-
throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
|
|
1812
2358
|
}
|
|
1813
2359
|
// The transaction was created with coordinatedRetry:true (see
|
|
1814
2360
|
// getReadTxn), so commit() can resolve to RETRY_NOW_VALUE. That
|
|
@@ -1817,6 +2363,11 @@ class DatabaseTransaction {
|
|
|
1817
2363
|
this.assertDatabaseCommitAllowed(transaction);
|
|
1818
2364
|
this.markNativeCommitSubmitted();
|
|
1819
2365
|
commitResolution = transaction.commit();
|
|
2366
|
+
this.setHandleCommitInFlight(transaction, true);
|
|
2367
|
+
if (this.hasLeaseProtectedWrite) {
|
|
2368
|
+
hadLeaseProtectedWrite = true;
|
|
2369
|
+
this.retainTransactionLockHolds(transaction);
|
|
2370
|
+
}
|
|
1820
2371
|
// Record how long this commit stays outstanding (submit → settle) as a distribution
|
|
1821
2372
|
// metric. This is the same clock the overload check uses (trackOutstandingCommit
|
|
1822
2373
|
// stamps each attempt at submit), so a rising p99/p999 is the leading indicator for the
|
|
@@ -1836,7 +2387,10 @@ class DatabaseTransaction {
|
|
|
1836
2387
|
try {
|
|
1837
2388
|
commitResolution = transaction.abort();
|
|
1838
2389
|
}
|
|
1839
|
-
catch {
|
|
2390
|
+
catch (abortError) {
|
|
2391
|
+
if (abortError?.code === 'ERR_TRANSACTION_ABANDONED') {
|
|
2392
|
+
harperLogger.error?.(`Aborted a transaction with a durable log entry (empty write set), ${describeCommitIdentity(this.db, this.startedFrom, transaction)}`, abortError);
|
|
2393
|
+
}
|
|
1840
2394
|
// The transaction has uncommitted writes that were already cleared from
|
|
1841
2395
|
// this.writes by a concurrent immediate-commit path (e.g. writes made with
|
|
1842
2396
|
// an explicitly-reused closed transaction). Those writes are handled by the
|
|
@@ -1849,38 +2403,53 @@ class DatabaseTransaction {
|
|
|
1849
2403
|
// Read the table off the write itself, not this.db, which is whichever table first
|
|
1850
2404
|
// claimed this per-database transaction in txnForContext and so can name the wrong
|
|
1851
2405
|
// table when a transaction spans more than one table in the same database.
|
|
1852
|
-
trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
|
|
2406
|
+
trackOutstandingCommit(commitResolution, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
|
|
1853
2407
|
// 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
|
-
|
|
2408
|
+
// root's clock rather than restart it (or the per-handle clock on an unchained
|
|
2409
|
+
// ImmediateTransaction), so only the first submission stamps.
|
|
2410
|
+
const carrier = this.budgetCarrier(transaction);
|
|
2411
|
+
if (carrier.commitStartedAt == null)
|
|
2412
|
+
carrier.commitStartedAt = performance.now();
|
|
1858
2413
|
const completions = [];
|
|
1859
2414
|
const commitOutcome = commitResolution.then((commitResult) => {
|
|
2415
|
+
const txnWithRetry = transaction;
|
|
1860
2416
|
if (commitResult === RETRY_NOW_VALUE) {
|
|
1861
|
-
this.
|
|
1862
|
-
|
|
2417
|
+
this.setHandleCommitInFlight(transaction, false);
|
|
2418
|
+
const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
|
|
2419
|
+
if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
|
|
2420
|
+
this.retries = retries;
|
|
2421
|
+
harperLogger.debug?.('coordinated retry', transaction.id, retries);
|
|
1863
2422
|
// Mark this specific native transaction as a retry so RocksTransactionLogStore
|
|
1864
2423
|
// 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
|
-
|
|
2424
|
+
txnWithRetry.isRetry = true;
|
|
2425
|
+
const logged = hasDurableLog(transaction);
|
|
2426
|
+
const pastBudget = this.elapsedPastCommitBudget(transaction);
|
|
2427
|
+
if (pastBudget) {
|
|
2428
|
+
if (logged)
|
|
2429
|
+
this.noteOverdueLoggedCommit(transaction, pastBudget);
|
|
2430
|
+
else
|
|
2431
|
+
this.abandonCommitAfterDeadline(transaction, pastBudget);
|
|
2432
|
+
}
|
|
2433
|
+
if (retries > MAX_RETRIES && !this.sourceApply && !logged) {
|
|
2434
|
+
// giving up: poison and abort the whole linked chain so no link leaks its native
|
|
2435
|
+
// handle / read snapshot — or any unpublished transaction-log position — until GC.
|
|
2436
|
+
this.abortChainAfterRetries(transaction);
|
|
2437
|
+
throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} coordinated retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
|
|
2438
|
+
}
|
|
2439
|
+
if ((this.sourceApply || logged) && retries > 0 && retries % MAX_RETRIES === 0) {
|
|
2440
|
+
harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} coordinated retries; continuing to retry`);
|
|
2441
|
+
}
|
|
2442
|
+
// Randomized so two uncapped logged commits crossing on the same keys stop waking in lockstep.
|
|
2443
|
+
if (logged && retries > 2) {
|
|
2444
|
+
const backoff = (0, promises_1.setTimeout)(Math.ceil(Math.min(retries * 2, MAX_RETRY_DELAY_MS) * Math.random()));
|
|
2445
|
+
trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
|
|
2446
|
+
return backoff.then(() => this.commit({ ...options, transaction }));
|
|
1881
2447
|
}
|
|
1882
2448
|
return this.commit({ ...options, transaction });
|
|
1883
2449
|
}
|
|
2450
|
+
const committedTransaction = transaction;
|
|
2451
|
+
const committedImmediate = Boolean(committedTransaction?.immediateCommit);
|
|
2452
|
+
this.setHandleCommitted(transaction);
|
|
1884
2453
|
// onCommit may be async (e.g. RocksTransactionLogStore emits 'aftercommit'). Surface a
|
|
1885
2454
|
// rejection — or a synchronous throw — via logging rather than failing the commit, since
|
|
1886
2455
|
// the write is already durable.
|
|
@@ -1892,7 +2461,14 @@ class DatabaseTransaction {
|
|
|
1892
2461
|
catch (error) {
|
|
1893
2462
|
harperLogger.warn?.('onCommit handler failed after commit', error);
|
|
1894
2463
|
}
|
|
1895
|
-
if (this.
|
|
2464
|
+
if (this.recordLocks)
|
|
2465
|
+
this.noteCommittedLockVersions(committedTransaction);
|
|
2466
|
+
releaseTransactionLockHolds(transaction);
|
|
2467
|
+
if (committedImmediate
|
|
2468
|
+
? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasSiblingCommitInFlight(committedTransaction)
|
|
2469
|
+
: !this.saveCommits || !this.hasSiblingCommitInFlight(committedTransaction))
|
|
2470
|
+
this.releaseRecordLocks();
|
|
2471
|
+
if (this.next && !committedImmediate) {
|
|
1896
2472
|
// never forward options.transaction (a retry/replay round's HEAD-store handle) to
|
|
1897
2473
|
// the next store — it must commit its own writes through its own transaction
|
|
1898
2474
|
let nextCommit;
|
|
@@ -1915,7 +2491,7 @@ class DatabaseTransaction {
|
|
|
1915
2491
|
// if we want to wait for replication confirmation, we need to track the transaction times
|
|
1916
2492
|
// and when replication notifications come in, we count the number of confirms until we reach the desired number
|
|
1917
2493
|
const databaseName = this.writes[0]?.store.rootStore.databaseName;
|
|
1918
|
-
const lastEntry = this.lastConfirmableEntry();
|
|
2494
|
+
const lastEntry = this.lastConfirmableEntry(true);
|
|
1919
2495
|
if (confirmReplication && databaseName && lastEntry) {
|
|
1920
2496
|
completions.push(confirmReplication(databaseName, lastEntry.version, this.replicatedConfirmation));
|
|
1921
2497
|
}
|
|
@@ -1926,24 +2502,36 @@ class DatabaseTransaction {
|
|
|
1926
2502
|
// later write retained survives). Deferred until here so a retry that *would* have
|
|
1927
2503
|
// referenced the blob can flip skipped/superseded back to false first.
|
|
1928
2504
|
for (const write of this.writes) {
|
|
1929
|
-
if (write?.
|
|
2505
|
+
if (!write?.saved || !write.savedBlobs)
|
|
2506
|
+
continue;
|
|
2507
|
+
if (committedImmediate
|
|
2508
|
+
? write.stagedOn !== committedTransaction
|
|
2509
|
+
: Boolean(write.stagedOn) && write.stagedOn !== committedTransaction)
|
|
2510
|
+
continue;
|
|
2511
|
+
if ((write.skipped &&
|
|
2512
|
+
!(write.blobsAuditReferenced &&
|
|
2513
|
+
(write.appendedTo === committedTransaction || hasDurableLog(write.appendedTo)))) ||
|
|
2514
|
+
(write.superseded && !write.blobsAuditReferenced))
|
|
1930
2515
|
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
1931
2516
|
}
|
|
1932
|
-
|
|
1933
|
-
this.noteCommittedLockVersions();
|
|
2517
|
+
const committedStagedTxnTime = this.getHandleStagedTxnTime(committedTransaction);
|
|
1934
2518
|
// now reset transactions tracking; this transaction be reused and committed again
|
|
1935
|
-
this.
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2519
|
+
this.clearWrites(committedTransaction);
|
|
2520
|
+
if (!committedImmediate) {
|
|
2521
|
+
this.retries = 0;
|
|
2522
|
+
if (options.doneWriting)
|
|
2523
|
+
this.endScopeOwnership();
|
|
2524
|
+
this.releaseContext(!!options.doneWriting);
|
|
2525
|
+
}
|
|
2526
|
+
let txnTime = committedStagedTxnTime || this.timestamp || 0;
|
|
2527
|
+
if ((!committedImmediate || this.open !== exports.TRANSACTION_STATE.OPEN) &&
|
|
2528
|
+
this.writes.length === 0 &&
|
|
2529
|
+
!this.hasActiveCommitHandle())
|
|
2530
|
+
this.timestamp = 0; // reset the timestamp as well
|
|
1943
2531
|
return Promise.all(completions).then(() => {
|
|
1944
2532
|
// Only once the chained store's commit has settled, as on the synchronous path: a
|
|
1945
2533
|
// partially failed mid-scope commit must not leave the scope resumable.
|
|
1946
|
-
this.completeMidScopeCommit(options);
|
|
2534
|
+
this.completeMidScopeCommit(options, committedTransaction);
|
|
1947
2535
|
return {
|
|
1948
2536
|
txnTime,
|
|
1949
2537
|
};
|
|
@@ -1951,10 +2539,14 @@ class DatabaseTransaction {
|
|
|
1951
2539
|
// As on the synchronous path: a completion that failed (a chained store's commit,
|
|
1952
2540
|
// a replication confirmation) leaves this commit partly landed, so ownership goes
|
|
1953
2541
|
// with it rather than letting a later commit rotate on top.
|
|
1954
|
-
|
|
2542
|
+
committedTransaction.committed = true;
|
|
2543
|
+
if (!committedImmediate)
|
|
2544
|
+
this.endScopeOwnership();
|
|
1955
2545
|
throw error;
|
|
1956
2546
|
});
|
|
1957
2547
|
}, (error) => {
|
|
2548
|
+
const txnWithRetry = transaction;
|
|
2549
|
+
this.setHandleCommitInFlight(transaction, false);
|
|
1958
2550
|
// Coordinated transactions surface conflicts as RETRY_NOW (handled in the
|
|
1959
2551
|
// resolve branch above) and never reach here with ERR_BUSY. But not every
|
|
1960
2552
|
// write transaction is coordinated — a write that reaches save() with no
|
|
@@ -1971,8 +2563,10 @@ class DatabaseTransaction {
|
|
|
1971
2563
|
if (error.code === 'ERR_BUSY' || error.code === 'ERR_TRY_AGAIN') {
|
|
1972
2564
|
// if the transaction failed due to concurrent changes, we need to retry. First record this as an increased risk of contention/retry
|
|
1973
2565
|
// for future transactions
|
|
1974
|
-
|
|
1975
|
-
|
|
2566
|
+
const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
|
|
2567
|
+
if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
|
|
2568
|
+
this.retries = retries;
|
|
2569
|
+
harperLogger.debug?.('retrying', transaction.id, retries);
|
|
1976
2570
|
// ERR_BUSY and ERR_TRY_AGAIN are both retried by recommitting the SAME native
|
|
1977
2571
|
// transaction. ERR_BUSY recovers because the save loop re-writes each key, re-tracking
|
|
1978
2572
|
// it at the current sequence. ERR_TRY_AGAIN — a snapshot stranded outside the memtable
|
|
@@ -1985,35 +2579,47 @@ class DatabaseTransaction {
|
|
|
1985
2579
|
// already-staged change-feed entry publishes only when the retry really commits — no
|
|
1986
2580
|
// premature publish, and no fresh-transaction replay that would drop the entry.
|
|
1987
2581
|
// Mark the native transaction as a retry so RocksTransactionLogStore skips re-staging entries.
|
|
1988
|
-
|
|
2582
|
+
txnWithRetry.isRetry = true;
|
|
2583
|
+
if (error?.hasLog === true) {
|
|
2584
|
+
txnWithRetry.hasStagedLog = true;
|
|
2585
|
+
}
|
|
2586
|
+
const logged = hasDurableLog(transaction, error);
|
|
1989
2587
|
// Before the backoff gate below: the budget can already be spent on the first
|
|
1990
2588
|
// retry when an earlier store in this chain consumed it.
|
|
1991
|
-
const pastBudget = this.elapsedPastCommitBudget();
|
|
1992
|
-
if (pastBudget)
|
|
1993
|
-
|
|
1994
|
-
|
|
2589
|
+
const pastBudget = this.elapsedPastCommitBudget(transaction);
|
|
2590
|
+
if (pastBudget) {
|
|
2591
|
+
if (logged)
|
|
2592
|
+
this.noteOverdueLoggedCommit(transaction, pastBudget);
|
|
2593
|
+
else
|
|
2594
|
+
this.abandonCommitAfterDeadline(transaction, pastBudget);
|
|
2595
|
+
}
|
|
2596
|
+
if (retries > 2) {
|
|
1995
2597
|
// Transactions applying data from a canonical source of truth (replication peer or
|
|
1996
2598
|
// external caching source) must never drop a write on a transient conflict: there is no
|
|
1997
2599
|
// re-subscribe / sequence-id-resume path, so a dropped write would leave this node
|
|
1998
2600
|
// permanently diverged (harper-pro#348). Such transactions retry without a cap; the source
|
|
1999
2601
|
// 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
|
-
}
|
|
2602
|
+
// compounding. Durably logged transactions likewise retry without a cap so a written
|
|
2603
|
+
// transaction-log entry is never abandoned.
|
|
2604
|
+
const neverDropOnConflict = this.sourceApply || logged;
|
|
2605
|
+
if (retries > MAX_RETRIES && !neverDropOnConflict) {
|
|
2606
|
+
// giving up: poison and abort the whole linked chain so no link leaks its native
|
|
2607
|
+
// handle / read snapshot until GC.
|
|
2608
|
+
this.abortChainAfterRetries(transaction);
|
|
2609
|
+
throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
|
|
2610
|
+
}
|
|
2611
|
+
// Uncapped retry can otherwise stall silently (debug logging is off in production);
|
|
2612
|
+
// surface periodic visibility into a stalled source-apply or logged commit.
|
|
2613
|
+
if (neverDropOnConflict && retries % MAX_RETRIES === 0) {
|
|
2614
|
+
harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} retries; continuing to retry`);
|
|
2014
2615
|
}
|
|
2015
2616
|
// start delaying, back off to try to space out transactions and avoid excessive conflicts
|
|
2016
|
-
|
|
2617
|
+
const backoff = (0, promises_1.setTimeout)(Math.min(retries * retries, MAX_RETRY_DELAY_MS));
|
|
2618
|
+
if (logged) {
|
|
2619
|
+
// waitForDrain() must outlast the backoff too
|
|
2620
|
+
trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
|
|
2621
|
+
}
|
|
2622
|
+
return backoff.then(() => this.commit({ ...options, transaction }));
|
|
2017
2623
|
}
|
|
2018
2624
|
return this.commit({ ...options, transaction }); // try again
|
|
2019
2625
|
}
|
|
@@ -2021,54 +2627,87 @@ class DatabaseTransaction {
|
|
|
2021
2627
|
// terminal (non-conflict) failure: release the native handle so it doesn't leak;
|
|
2022
2628
|
// usually already released by the failed commit itself, abort for the unexpected
|
|
2023
2629
|
// 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);
|
|
2630
|
+
if (error?.hasLog) {
|
|
2631
|
+
txnWithRetry.isRetry = true;
|
|
2632
|
+
txnWithRetry.hasStagedLog = true;
|
|
2029
2633
|
}
|
|
2634
|
+
abortNativeTransaction(transaction, 'aborting transaction after failed commit', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2030
2635
|
// A terminal failure is just as final as a success — release the context's
|
|
2031
2636
|
// back-reference while handling the native transaction. performCommit's outer
|
|
2032
2637
|
// rejection handler then aborts the wrapper and its linked chain.
|
|
2033
2638
|
// A failed commit must never be followed by a resumed segment: this generation is
|
|
2034
2639
|
// finished and its durability is unknown, so ownership goes with it.
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2640
|
+
if (!isImmediateCommit &&
|
|
2641
|
+
!this.hasWriteOwnedByActiveCommit(transaction, this.hasSiblingCommitInFlight(transaction), true, true)) {
|
|
2642
|
+
this.endScopeOwnership();
|
|
2643
|
+
this.releaseContext(!!options.doneWriting);
|
|
2644
|
+
}
|
|
2645
|
+
if (!hasDurableLog(transaction, error)) {
|
|
2646
|
+
for (const write of this.writes) {
|
|
2647
|
+
if (!write?.saved ||
|
|
2648
|
+
!write.savedBlobs ||
|
|
2649
|
+
(isImmediateCommit
|
|
2650
|
+
? write.stagedOn !== transaction
|
|
2651
|
+
: Boolean(write.stagedOn) && write.stagedOn !== transaction))
|
|
2652
|
+
continue;
|
|
2653
|
+
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
this.clearWrites(transaction);
|
|
2657
|
+
releaseTransactionLockHolds(transaction);
|
|
2658
|
+
if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
|
|
2659
|
+
!this.hasActiveCommitHandle() &&
|
|
2660
|
+
this.writes.length === 0)
|
|
2661
|
+
this.releaseRecordLocks();
|
|
2662
|
+
if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle())
|
|
2663
|
+
this.timestamp = 0;
|
|
2039
2664
|
throw error;
|
|
2040
2665
|
}
|
|
2041
2666
|
});
|
|
2042
|
-
//
|
|
2043
|
-
//
|
|
2044
|
-
//
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
chainRoot.commitStartedAt = undefined;
|
|
2050
|
-
return resolution;
|
|
2051
|
-
}, (error) => {
|
|
2052
|
-
chainRoot.commitStartedAt = undefined;
|
|
2667
|
+
// Release any lock holds remaining if `commitOutcome` rejects (for example when a chained
|
|
2668
|
+
// store's commit or a completion fails) through the returned promise rather than a second
|
|
2669
|
+
// subscriber that would mark an unawaited commit rejection as handled.
|
|
2670
|
+
if (!hadLeaseProtectedWrite)
|
|
2671
|
+
return commitOutcome;
|
|
2672
|
+
return commitOutcome.then(undefined, (error) => {
|
|
2673
|
+
releaseTransactionLockHolds(transaction);
|
|
2053
2674
|
throw error;
|
|
2054
2675
|
});
|
|
2055
2676
|
}
|
|
2056
2677
|
for (const write of this.writes) {
|
|
2057
|
-
if (write?.
|
|
2678
|
+
if (!write?.saved)
|
|
2679
|
+
continue;
|
|
2680
|
+
if (isImmediateCommit
|
|
2681
|
+
? write.stagedOn !== transaction
|
|
2682
|
+
: this.hasSiblingCommitInFlight(transaction) && Boolean(write.stagedOn) && write.stagedOn !== transaction)
|
|
2683
|
+
continue;
|
|
2684
|
+
if (write.savedBlobs &&
|
|
2685
|
+
((write.skipped &&
|
|
2686
|
+
!(write.blobsAuditReferenced && (write.appendedTo === transaction || hasDurableLog(write.appendedTo)))) ||
|
|
2687
|
+
(write.superseded && !write.blobsAuditReferenced)))
|
|
2058
2688
|
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
2059
2689
|
}
|
|
2060
2690
|
if (this.recordLocks)
|
|
2061
|
-
this.noteCommittedLockVersions();
|
|
2062
|
-
this.
|
|
2063
|
-
this.
|
|
2064
|
-
if (
|
|
2065
|
-
this.
|
|
2066
|
-
|
|
2691
|
+
this.noteCommittedLockVersions(transaction);
|
|
2692
|
+
const committedStagedTxnTime = this.getHandleStagedTxnTime(transaction);
|
|
2693
|
+
this.clearWrites(transaction);
|
|
2694
|
+
if (isImmediateCommit
|
|
2695
|
+
? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle() && this.writes.length === 0
|
|
2696
|
+
: !this.saveCommits || (!this.hasActiveCommitHandle() && this.writes.length === 0))
|
|
2697
|
+
this.releaseRecordLocks();
|
|
2698
|
+
if (!isImmediateCommit) {
|
|
2699
|
+
if (options.doneWriting)
|
|
2700
|
+
this.endScopeOwnership();
|
|
2701
|
+
this.releaseContext(!!options.doneWriting);
|
|
2702
|
+
}
|
|
2067
2703
|
const txnResolution = {
|
|
2068
|
-
txnTime: this.timestamp,
|
|
2704
|
+
txnTime: committedStagedTxnTime || this.timestamp,
|
|
2069
2705
|
};
|
|
2070
|
-
|
|
2071
|
-
|
|
2706
|
+
if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
|
|
2707
|
+
this.writes.length === 0 &&
|
|
2708
|
+
!this.hasActiveCommitHandle())
|
|
2709
|
+
this.timestamp = 0;
|
|
2710
|
+
if (this.next && !isImmediateCommit) {
|
|
2072
2711
|
// now run any other transactions
|
|
2073
2712
|
options.timestamp = txnResolution.txnTime;
|
|
2074
2713
|
// as above: the next store must not inherit this store's explicit native transaction
|
|
@@ -2087,7 +2726,7 @@ class DatabaseTransaction {
|
|
|
2087
2726
|
return nextResolution?.then((nextResolution) => {
|
|
2088
2727
|
// Only once the chained store's own commit has SETTLED: rotating first would leave the
|
|
2089
2728
|
// scope resumable after a partially failed mid-scope commit.
|
|
2090
|
-
this.completeMidScopeCommit(options);
|
|
2729
|
+
this.completeMidScopeCommit(options, transaction);
|
|
2091
2730
|
return {
|
|
2092
2731
|
txnTime: txnResolution.txnTime,
|
|
2093
2732
|
next: nextResolution,
|
|
@@ -2102,18 +2741,20 @@ class DatabaseTransaction {
|
|
|
2102
2741
|
});
|
|
2103
2742
|
txnResolution.next = nextResolution;
|
|
2104
2743
|
}
|
|
2105
|
-
this.completeMidScopeCommit(options);
|
|
2744
|
+
this.completeMidScopeCommit(options, transaction);
|
|
2106
2745
|
return txnResolution;
|
|
2107
2746
|
});
|
|
2108
2747
|
}
|
|
2109
2748
|
catch (error) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2749
|
+
if (inCommitPhase)
|
|
2750
|
+
this.setCommitPhase(false);
|
|
2751
|
+
this.abortAfterCommitError(error, transaction);
|
|
2112
2752
|
}
|
|
2113
2753
|
if (commitResult?.then)
|
|
2114
2754
|
return commitResult.catch((error) => {
|
|
2115
|
-
|
|
2116
|
-
|
|
2755
|
+
if (inCommitPhase)
|
|
2756
|
+
this.setCommitPhase(false);
|
|
2757
|
+
this.abortAfterCommitError(error, transaction);
|
|
2117
2758
|
});
|
|
2118
2759
|
return commitResult;
|
|
2119
2760
|
}
|
|
@@ -2141,8 +2782,12 @@ class DatabaseTransaction {
|
|
|
2141
2782
|
this.#scopeOwned = false;
|
|
2142
2783
|
this.snapshotFree = false;
|
|
2143
2784
|
}
|
|
2144
|
-
completeMidScopeCommit(options) {
|
|
2785
|
+
completeMidScopeCommit(options, committedTransaction) {
|
|
2786
|
+
if (committedTransaction?.immediateCommit)
|
|
2787
|
+
return;
|
|
2145
2788
|
this.next = null;
|
|
2789
|
+
if (this.saveCommits && this.hasActiveCommitHandle())
|
|
2790
|
+
return;
|
|
2146
2791
|
this.rotateAfterMidScopeCommit(options);
|
|
2147
2792
|
}
|
|
2148
2793
|
/** See completeMidScopeCommit, which is the only caller and carries the reasoning. */
|
|
@@ -2157,82 +2802,147 @@ class DatabaseTransaction {
|
|
|
2157
2802
|
this.open = exports.TRANSACTION_STATE.OPEN;
|
|
2158
2803
|
this.snapshotFree = true;
|
|
2159
2804
|
this.writesAbandoned = false;
|
|
2805
|
+
if (this.#context && (this.#context.transaction === this || this.#context.transaction === exports.RELEASED_TRANSACTION)) {
|
|
2806
|
+
this.#context.transaction = this;
|
|
2807
|
+
}
|
|
2160
2808
|
}
|
|
2161
2809
|
assertDatabaseCommitAllowed(transaction) {
|
|
2810
|
+
if (hasDurableLog(transaction))
|
|
2811
|
+
return;
|
|
2162
2812
|
const rootStore = this.writes[0]?.store?.rootStore;
|
|
2163
2813
|
if (!databaseCommitsSuspended(rootStore))
|
|
2164
2814
|
return;
|
|
2165
2815
|
const retryable = !this.root && !this.snapshotFree;
|
|
2166
2816
|
if (this.transaction === transaction)
|
|
2167
2817
|
this.detachOwnedTransaction();
|
|
2168
|
-
abortNativeTransaction(transaction, 'aborting a transaction while its database is closing');
|
|
2818
|
+
abortNativeTransaction(transaction, 'aborting a transaction while its database is closing', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2169
2819
|
try {
|
|
2170
|
-
this.abort();
|
|
2820
|
+
this.abort(false, true, transaction);
|
|
2171
2821
|
}
|
|
2172
2822
|
catch (error) {
|
|
2173
2823
|
harperLogger.debug?.('cleaning up a transaction while its database is closing', error);
|
|
2174
2824
|
}
|
|
2175
2825
|
throw new hdbError_ts_1.DatabaseClosingError(rootStore?.databaseName ?? rootStore?.path ?? 'unknown', retryable);
|
|
2176
2826
|
}
|
|
2177
|
-
abortAfterCommitError(error) {
|
|
2827
|
+
abortAfterCommitError(error, targetTransaction) {
|
|
2178
2828
|
try {
|
|
2829
|
+
const retryTxn = targetTransaction;
|
|
2830
|
+
const retryTxnCommitted = this.isHandleCommitted(retryTxn);
|
|
2831
|
+
const retryTxnInFlight = this.isHandleCommitInFlight(retryTxn);
|
|
2832
|
+
if (retryTxn && retryTxnCommitted)
|
|
2833
|
+
retryTxn.committed = true;
|
|
2834
|
+
if (retryTxn)
|
|
2835
|
+
this.releaseActiveCommitHandle(retryTxn);
|
|
2836
|
+
if (retryTxn && retryTxn !== this.transaction && !retryTxnCommitted && !retryTxnInFlight) {
|
|
2837
|
+
releaseTransactionLockHolds(retryTxn);
|
|
2838
|
+
abortNativeTransaction(retryTxn, 'aborting a retry transaction after pre-commit failure', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2839
|
+
}
|
|
2179
2840
|
// Not "always retain": abort(true) keeps the native handle only while read iterators still
|
|
2180
2841
|
// own it. resources/transaction.ts's own post-error abort passes the same argument, so the
|
|
2181
2842
|
// two layers cannot contradict each other one frame apart.
|
|
2182
|
-
this.abort(true);
|
|
2843
|
+
this.abort(true, true, targetTransaction);
|
|
2183
2844
|
}
|
|
2184
2845
|
catch (abortError) {
|
|
2185
2846
|
harperLogger.debug?.('aborting transaction after a failed commit', abortError);
|
|
2186
2847
|
}
|
|
2187
2848
|
throw error;
|
|
2188
2849
|
}
|
|
2189
|
-
abort(retainReadTransaction = false, cascade = true) {
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2850
|
+
abort(retainReadTransaction = false, cascade = true, targetTransaction) {
|
|
2851
|
+
const nativeTxn = targetTransaction ?? this.transaction;
|
|
2852
|
+
const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
|
|
2853
|
+
const hasSiblingInFlight = this.hasSiblingCommitInFlight(targetTransaction);
|
|
2854
|
+
const preserveActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true);
|
|
2855
|
+
const preserveNonImmediateActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true, true);
|
|
2856
|
+
const cascadeNext = cascade && !preserveNonImmediateActiveWrites;
|
|
2857
|
+
const next = cascadeNext ? this.next : undefined;
|
|
2858
|
+
if (cascadeNext && !retainReadTransaction)
|
|
2192
2859
|
this.next = null;
|
|
2193
2860
|
try {
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
this.
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
this.
|
|
2861
|
+
const ownsCurrentTransaction = !isImmediateTarget && (!hasSiblingInFlight || !targetTransaction || this.transaction === targetTransaction);
|
|
2862
|
+
const currentTxn = this.transaction;
|
|
2863
|
+
const currentTxnActive = Boolean(currentTxn) &&
|
|
2864
|
+
!currentTxn.abortedByWrapper &&
|
|
2865
|
+
!this.isHandleCommitted(currentTxn) &&
|
|
2866
|
+
(this.isHandleCommitInFlight(currentTxn) ||
|
|
2867
|
+
hasDurableLog(currentTxn) ||
|
|
2868
|
+
(Boolean(targetTransaction) && currentTxn !== targetTransaction && this.isActiveCommitHandle(currentTxn)));
|
|
2869
|
+
if (ownsCurrentTransaction && !currentTxnActive) {
|
|
2870
|
+
if (this.transaction)
|
|
2871
|
+
this.releaseActiveCommitHandle(this.transaction);
|
|
2872
|
+
const hasOpenReadIterator = retainReadTransaction &&
|
|
2873
|
+
this.transaction &&
|
|
2874
|
+
(this.readTxnsUsed > 1 || (this.baseReadRefConsumed && this.readTxnsUsed > 0));
|
|
2875
|
+
if (hasOpenReadIterator) {
|
|
2876
|
+
this.releaseRetainedWriteIntents();
|
|
2877
|
+
if (!this.baseReadRefConsumed) {
|
|
2878
|
+
this.doneReadTxn();
|
|
2879
|
+
this.baseReadRefConsumed = true;
|
|
2880
|
+
}
|
|
2202
2881
|
}
|
|
2882
|
+
else {
|
|
2883
|
+
while (this.readTxnsUsed > 0)
|
|
2884
|
+
this.doneReadTxn(); // release the read snapshot when we abort, we assume we don't need it
|
|
2885
|
+
}
|
|
2886
|
+
// A write-only transaction never took a read reference (getReadTxn was never called), so the
|
|
2887
|
+
// loop above releases nothing even though save() created a native handle. Keep an iterator's
|
|
2888
|
+
// handle alive, but release every other remaining handle through the shared cleanup path.
|
|
2889
|
+
if (this.transaction && !hasOpenReadIterator)
|
|
2890
|
+
this.releaseReadTxn();
|
|
2203
2891
|
}
|
|
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();
|
|
2892
|
+
if (!isImmediateTarget)
|
|
2893
|
+
this.open = exports.TRANSACTION_STATE.CLOSED;
|
|
2894
|
+
this.drainCompletions(targetTransaction, preserveActiveWrites);
|
|
2895
|
+
const targetLogged = hasDurableLog(targetTransaction);
|
|
2216
2896
|
for (const write of this.writes) {
|
|
2217
|
-
if (write
|
|
2897
|
+
if (!write)
|
|
2898
|
+
continue;
|
|
2899
|
+
if (preserveActiveWrites && this.isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, true))
|
|
2900
|
+
continue;
|
|
2901
|
+
if (targetLogged &&
|
|
2902
|
+
write.saved &&
|
|
2903
|
+
(isImmediateTarget
|
|
2904
|
+
? write.stagedOn === targetTransaction
|
|
2905
|
+
: !write.stagedOn || write.stagedOn === targetTransaction))
|
|
2906
|
+
continue;
|
|
2907
|
+
if (write.savedBlobs)
|
|
2218
2908
|
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
2219
2909
|
}
|
|
2220
2910
|
}
|
|
2221
2911
|
finally {
|
|
2222
2912
|
try {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2913
|
+
if (!isImmediateTarget)
|
|
2914
|
+
this.open = exports.TRANSACTION_STATE.CLOSED;
|
|
2915
|
+
const endScopeOnAbort = !isImmediateTarget && (!targetTransaction || !preserveNonImmediateActiveWrites);
|
|
2916
|
+
if (endScopeOnAbort)
|
|
2917
|
+
this.endScopeOwnership(); // the scope is over; nothing may rotate this instance again
|
|
2918
|
+
this.clearWrites(targetTransaction, true);
|
|
2919
|
+
if (targetTransaction) {
|
|
2920
|
+
releaseTransactionLockHolds(targetTransaction);
|
|
2921
|
+
}
|
|
2922
|
+
if ((!isImmediateTarget || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle()) {
|
|
2923
|
+
this.timestamp = 0; // a lock stamp pinned for this write set must not leak into the next
|
|
2924
|
+
this.retries = 0;
|
|
2925
|
+
}
|
|
2926
|
+
if ((isImmediateTarget
|
|
2927
|
+
? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle()
|
|
2928
|
+
: !preserveActiveWrites) &&
|
|
2929
|
+
this.writes.length === 0) {
|
|
2930
|
+
releaseTransactionLockHolds(nativeTxn);
|
|
2931
|
+
if (this.transaction && this.transaction !== nativeTxn)
|
|
2932
|
+
releaseTransactionLockHolds(this.transaction);
|
|
2933
|
+
this.releaseRecordLocks();
|
|
2934
|
+
}
|
|
2227
2935
|
// A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
|
|
2228
2936
|
// Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
|
|
2229
2937
|
// starting a fresh one) so the rest of the logical operation fails atomically via the
|
|
2230
2938
|
// poison check in addWrite()/commit(), rather than silently landing a later write on a
|
|
2231
2939
|
// brand-new transaction after an earlier one was rolled back (#1411). Releasing here would
|
|
2232
2940
|
// make that check see `undefined?.timedOut` and take the "start fresh" branch instead.
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
this.
|
|
2941
|
+
if (endScopeOnAbort) {
|
|
2942
|
+
this.releaseContext(!this.timedOut && !this.disconnected);
|
|
2943
|
+
if (!this.root)
|
|
2944
|
+
this.releaseRequestAbortListener();
|
|
2945
|
+
}
|
|
2236
2946
|
}
|
|
2237
2947
|
finally {
|
|
2238
2948
|
if (next && !(next.nativeCommitSubmitted && next.commitsInFlight)) {
|
|
@@ -2259,10 +2969,10 @@ class DatabaseTransaction {
|
|
|
2259
2969
|
* Source-applied writes are exempt for the same reason they are exempt from the attempt cap:
|
|
2260
2970
|
* there is no resubscribe/sequence-resume path, so dropping one permanently diverges this node.
|
|
2261
2971
|
*/
|
|
2262
|
-
elapsedPastCommitBudget() {
|
|
2972
|
+
elapsedPastCommitBudget(transaction) {
|
|
2263
2973
|
if (this.sourceApply)
|
|
2264
2974
|
return 0;
|
|
2265
|
-
const startedAt =
|
|
2975
|
+
const startedAt = this.budgetCarrier(transaction).commitStartedAt;
|
|
2266
2976
|
if (startedAt == null)
|
|
2267
2977
|
return 0;
|
|
2268
2978
|
const elapsed = performance.now() - startedAt;
|
|
@@ -2283,18 +2993,54 @@ class DatabaseTransaction {
|
|
|
2283
2993
|
const elapsed = Math.round(elapsedMs);
|
|
2284
2994
|
const budget = this.commitConflictBudget();
|
|
2285
2995
|
const retryable = !this.root && !this.snapshotFree;
|
|
2996
|
+
const retries = headTransaction?.retries ?? this.retries;
|
|
2286
2997
|
if (allowStuckCommitLog('abandon', performance.now())) {
|
|
2287
|
-
|
|
2288
|
-
|
|
2998
|
+
let headTransactionId;
|
|
2999
|
+
try {
|
|
3000
|
+
headTransactionId = headTransaction?.id;
|
|
3001
|
+
}
|
|
3002
|
+
catch { }
|
|
3003
|
+
harperLogger.error?.(`Abandoning a write transaction: its commit has been in write-intent conflict for ${elapsed}ms ` +
|
|
3004
|
+
`(exceeds the ${budget}ms limit) across ${retries} retries, ` +
|
|
2289
3005
|
describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
|
|
2290
3006
|
`.` +
|
|
2291
|
-
(0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path,
|
|
3007
|
+
(0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
|
|
2292
3008
|
` Another transaction holds a conflicting write intent and has not completed; the request is ` +
|
|
2293
3009
|
`failed with a 503${retryable ? '' : ' (not retryable — an earlier store in this transaction already committed)'} ` +
|
|
2294
3010
|
`rather than waiting further.`);
|
|
2295
3011
|
}
|
|
2296
3012
|
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 ${
|
|
3013
|
+
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);
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* A durably logged commit past its conflict budget cannot be aborted; release its deferred
|
|
3017
|
+
* record-lock holds (`holdBudgetSpent` prevents re-retaining them) and log once per chain/cooldown.
|
|
3018
|
+
*/
|
|
3019
|
+
noteOverdueLoggedCommit(headTransaction, elapsedMs) {
|
|
3020
|
+
headTransaction.holdBudgetSpent = true;
|
|
3021
|
+
releaseTransactionLockHolds(headTransaction);
|
|
3022
|
+
const carrier = this.budgetCarrier(headTransaction);
|
|
3023
|
+
if (carrier.commitOverdueLogged)
|
|
3024
|
+
return;
|
|
3025
|
+
if (!allowStuckCommitLog('overdue', performance.now()))
|
|
3026
|
+
return;
|
|
3027
|
+
carrier.commitOverdueLogged = true;
|
|
3028
|
+
const elapsed = Math.round(elapsedMs);
|
|
3029
|
+
const budget = this.commitConflictBudget();
|
|
3030
|
+
const retries = headTransaction?.retries ?? this.retries;
|
|
3031
|
+
let headTransactionId;
|
|
3032
|
+
try {
|
|
3033
|
+
headTransactionId = headTransaction?.id;
|
|
3034
|
+
}
|
|
3035
|
+
catch { }
|
|
3036
|
+
harperLogger.error?.(`A durably logged write transaction is still in write-intent conflict after ${elapsed}ms ` +
|
|
3037
|
+
`(exceeds the ${budget}ms limit) across ${retries} retries, ` +
|
|
3038
|
+
describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
|
|
3039
|
+
`.` +
|
|
3040
|
+
(0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
|
|
3041
|
+
` Because its transaction-log entry is already durable on disk, it cannot be aborted and ` +
|
|
3042
|
+
`will continue retrying until the conflicting write intent releases (pinning the local ` +
|
|
3043
|
+
`transaction-log read watermark for subscriptions and replication while uncommitted).`);
|
|
2298
3044
|
}
|
|
2299
3045
|
/**
|
|
2300
3046
|
* Give up on a chain of linked transactions after exhausting conflict retries: poison every link
|
|
@@ -2307,36 +3053,51 @@ class DatabaseTransaction {
|
|
|
2307
3053
|
* from the replay transaction; retry exhaustion aborts both.
|
|
2308
3054
|
*/
|
|
2309
3055
|
abortChainAfterRetries(headTransaction) {
|
|
3056
|
+
const isImmediateCommit = Boolean(headTransaction?.immediateCommit);
|
|
3057
|
+
const hasSiblingInFlight = this.hasSiblingCommitInFlight(headTransaction);
|
|
3058
|
+
if (isImmediateCommit ||
|
|
3059
|
+
(hasSiblingInFlight && this.hasWriteOwnedByActiveCommit(headTransaction, hasSiblingInFlight, true, true))) {
|
|
3060
|
+
if (!isImmediateCommit && this.transaction === headTransaction)
|
|
3061
|
+
this.detachOwnedTransaction();
|
|
3062
|
+
releaseTransactionLockHolds(headTransaction);
|
|
3063
|
+
abortNativeTransaction(headTransaction, 'aborting conflicted transaction in chain after exhausting retries', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
3064
|
+
try {
|
|
3065
|
+
this.abort(false, false, headTransaction);
|
|
3066
|
+
}
|
|
3067
|
+
catch (abortError) {
|
|
3068
|
+
harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
|
|
3069
|
+
}
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
2310
3072
|
for (let txn = this; txn; txn = txn.next) {
|
|
2311
3073
|
txn.open = exports.TRANSACTION_STATE.CLOSED;
|
|
2312
3074
|
}
|
|
2313
|
-
for (let txn = this; txn; txn =
|
|
3075
|
+
for (let txn = this, next; txn; txn = next) {
|
|
3076
|
+
next = txn.next;
|
|
3077
|
+
txn.next = null;
|
|
2314
3078
|
// Detach first so the abort() below performs only non-native cleanup, and so its doneReadTxn
|
|
2315
3079
|
// loop cannot spin on a nulled handle. Not to avoid a double abort: rocksdb-js tolerates
|
|
2316
3080
|
// abort-after-abort, and it is abort-after-COMMIT that throws.
|
|
2317
|
-
const detached = txn
|
|
3081
|
+
const detached = txn === this &&
|
|
3082
|
+
hasSiblingInFlight &&
|
|
3083
|
+
txn.transaction &&
|
|
3084
|
+
txn.transaction !== headTransaction &&
|
|
3085
|
+
(this.isActiveCommitHandle(txn.transaction) || hasDurableLog(txn.transaction))
|
|
3086
|
+
? null
|
|
3087
|
+
: txn.detachOwnedTransaction();
|
|
2318
3088
|
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
|
-
}
|
|
3089
|
+
releaseTransactionLockHolds(committingTransaction);
|
|
3090
|
+
abortNativeTransaction(committingTransaction, 'aborting conflicted transaction in chain after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
|
|
2325
3091
|
// With outstanding iterators, the head owns a retained read handle while the retry commits
|
|
2326
3092
|
// through a separate replay handle. Both must be aborted after retry exhaustion.
|
|
2327
3093
|
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
|
-
}
|
|
3094
|
+
abortNativeTransaction(detached, 'aborting retained read transaction after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
|
|
2334
3095
|
}
|
|
2335
3096
|
try {
|
|
2336
3097
|
// abort() synchronously walks savedBlobs and can call write.store.getEntry(), which can throw
|
|
2337
3098
|
// (closed store, decode error). Catch and continue so one link's wrapper-cleanup failure can't
|
|
2338
3099
|
// strand later links' native handles — they were already detached/aborted above regardless.
|
|
2339
|
-
txn.abort();
|
|
3100
|
+
txn.abort(false, false, committingTransaction);
|
|
2340
3101
|
}
|
|
2341
3102
|
catch (abortError) {
|
|
2342
3103
|
harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
|
|
@@ -2430,18 +3191,7 @@ class DatabaseTransaction {
|
|
|
2430
3191
|
*/
|
|
2431
3192
|
poisonAfterStalledSubmittedCommit() {
|
|
2432
3193
|
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) {
|
|
3194
|
+
for (const txn of collectCommitLinks(root, this)) {
|
|
2445
3195
|
txn.postSubmitPoisoned = true;
|
|
2446
3196
|
// Not on a link an earlier poison already aborted: abort() cleared its write set, so excusing
|
|
2447
3197
|
// its continuation from the poison check would let it resume onto nothing and report success.
|
|
@@ -2465,7 +3215,7 @@ class DatabaseTransaction {
|
|
|
2465
3215
|
// throws. abort() rather than the native abort alone: it reclaims blobs a replayed write
|
|
2466
3216
|
// staged.
|
|
2467
3217
|
this.detachOwnedTransaction();
|
|
2468
|
-
abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed');
|
|
3218
|
+
abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed', this.writes[0]?.store ?? this.db, this.startedFrom);
|
|
2469
3219
|
try {
|
|
2470
3220
|
this.abort();
|
|
2471
3221
|
}
|
|
@@ -2505,10 +3255,12 @@ class ImmediateTransaction extends DatabaseTransaction {
|
|
|
2505
3255
|
}
|
|
2506
3256
|
save(...args) {
|
|
2507
3257
|
const operation = args[0]; // the staged write, not a transaction — commit() re-enters here with it
|
|
2508
|
-
if (
|
|
3258
|
+
if (operation?.deferSave)
|
|
3259
|
+
operation.deferSave = false;
|
|
3260
|
+
if (this.isCommitting || args[1] || args[3]) {
|
|
2509
3261
|
// Stage into the transaction commit() is committing (on a retry round, the one being retried)
|
|
2510
3262
|
// and reload within it; with no handle super.save() would open its own and nest a commit().
|
|
2511
|
-
super.save(operation, args[1], true);
|
|
3263
|
+
super.save(operation, args[1], true, args[3]);
|
|
2512
3264
|
}
|
|
2513
3265
|
else {
|
|
2514
3266
|
this.admitRequestWrite(); // a deferred save reaches here without passing through addWrite()
|
|
@@ -2542,7 +3294,7 @@ class ImmediateTransaction extends DatabaseTransaction {
|
|
|
2542
3294
|
return;
|
|
2543
3295
|
for (const [store, storeMap] of recordLocks) {
|
|
2544
3296
|
for (const [keyId, handle] of storeMap)
|
|
2545
|
-
if (handle.released)
|
|
3297
|
+
if (handle.released || handle.isExpired())
|
|
2546
3298
|
storeMap.delete(keyId);
|
|
2547
3299
|
if (storeMap.size === 0)
|
|
2548
3300
|
recordLocks.delete(store);
|
|
@@ -2704,6 +3456,12 @@ function reportIfLongLived(txn, thresholdMs, now, reportBudget) {
|
|
|
2704
3456
|
/** Diagnostic grace after a native submission outlives the ordinary transaction timeout. */
|
|
2705
3457
|
const MAX_DEFERRED_POISON_TICKS = 2;
|
|
2706
3458
|
const STALLED_COMMIT_LOG_MIN_INTERVAL_MS = 1000;
|
|
3459
|
+
// A logged retry parked on a write intent moves its retry counter at least every native park timeout,
|
|
3460
|
+
// so a counter that has not moved for two of them is a native attempt that never settled — whatever
|
|
3461
|
+
// the monitor's tick interval (STORAGE_MAXTRANSACTIONOPENTIME can be set far below the park timeout).
|
|
3462
|
+
const configuredParkTimeoutMs = Number(process.env.ROCKSDB_JS_PARK_TIMEOUT_MS);
|
|
3463
|
+
// Same acceptance rule as rocksdb-js: anything but a finite positive number falls back to its 5000 ms default.
|
|
3464
|
+
const LOGGED_RETRY_STALL_MS = 2 * (Number.isFinite(configuredParkTimeoutMs) && configuredParkTimeoutMs > 0 ? configuredParkTimeoutMs : 5000);
|
|
2707
3465
|
let lastStalledCommitWarningAt = -Infinity;
|
|
2708
3466
|
let lastStalledCommitErrorAt = -Infinity;
|
|
2709
3467
|
function submittedCommitStillInFlight(root) {
|
|
@@ -2717,12 +3475,36 @@ function submittedCommitStillInFlight(root) {
|
|
|
2717
3475
|
}
|
|
2718
3476
|
return false;
|
|
2719
3477
|
}
|
|
3478
|
+
// Every link the monitor would poison for a stalled commit, so the two walks agree on which links count.
|
|
3479
|
+
function collectCommitLinks(root, txn) {
|
|
3480
|
+
const links = new Set();
|
|
3481
|
+
const collect = (start) => {
|
|
3482
|
+
for (let link = start; link; link = link.next)
|
|
3483
|
+
links.add(link);
|
|
3484
|
+
};
|
|
3485
|
+
collect(root);
|
|
3486
|
+
collect(txn);
|
|
3487
|
+
collect(root.submittedLink);
|
|
3488
|
+
if (root.submittedLinks)
|
|
3489
|
+
for (const link of root.submittedLinks)
|
|
3490
|
+
collect(link);
|
|
3491
|
+
return links;
|
|
3492
|
+
}
|
|
3493
|
+
function chainDurableLogRetries(root, txn) {
|
|
3494
|
+
let retries = -1;
|
|
3495
|
+
for (const link of collectCommitLinks(root, txn)) {
|
|
3496
|
+
const linkRetries = link.activeDurableLogRetries();
|
|
3497
|
+
if (linkRetries >= 0)
|
|
3498
|
+
retries = Math.max(retries, 0) + linkRetries;
|
|
3499
|
+
}
|
|
3500
|
+
return retries;
|
|
3501
|
+
}
|
|
2720
3502
|
/**
|
|
2721
3503
|
* True when this tick must leave `txn` alone because some link has submitted writes whose native
|
|
2722
3504
|
* outcome is not known yet. Merely entering commit() is not enough: a wholly pre-submit transaction
|
|
2723
3505
|
* remains subject to shouldSpareCommitPhase() and its ordinary bounded grace.
|
|
2724
3506
|
*/
|
|
2725
|
-
function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
|
|
3507
|
+
function deferForCommitInFlight(txn, url, expiration = txnExpiration, tick = monitorTick) {
|
|
2726
3508
|
const root = txn.root ?? txn;
|
|
2727
3509
|
if (!txn.isChainCommitting() || !root.commitSubmitted)
|
|
2728
3510
|
return false;
|
|
@@ -2735,27 +3517,36 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
|
|
|
2735
3517
|
// diagnostic grace, nor report a legitimately slow one early.
|
|
2736
3518
|
const now = performance.now();
|
|
2737
3519
|
const deadline = (root.deferredPoisonDeadline ??= now + MAX_DEFERRED_POISON_TICKS * expiration);
|
|
3520
|
+
// A handle with a durable log entry retrying a conflict has a known outcome — it commits once the
|
|
3521
|
+
// conflicting intent clears — so while its retry counter keeps advancing it is neither stalled nor
|
|
3522
|
+
// poisoned, and noteOverdueLoggedCommit is the operator's signal. A counter that has not moved for
|
|
3523
|
+
// LOGGED_RETRY_STALL_MS is a native attempt that never settled, which is the stall handled below.
|
|
3524
|
+
const loggedRetries = chainDurableLogRetries(root, txn);
|
|
3525
|
+
let loggedRetry = false;
|
|
3526
|
+
if (loggedRetries >= 0) {
|
|
3527
|
+
if (root.loggedRetriesTick === tick) {
|
|
3528
|
+
loggedRetry = Boolean(root.loggedRetryAdvancing);
|
|
3529
|
+
}
|
|
3530
|
+
else {
|
|
3531
|
+
if (root.loggedRetriesSeen !== loggedRetries) {
|
|
3532
|
+
root.loggedRetriesSeen = loggedRetries;
|
|
3533
|
+
root.loggedRetriesChangedAt = now;
|
|
3534
|
+
}
|
|
3535
|
+
loggedRetry = now - root.loggedRetriesChangedAt < LOGGED_RETRY_STALL_MS;
|
|
3536
|
+
root.loggedRetriesTick = tick;
|
|
3537
|
+
root.loggedRetryAdvancing = loggedRetry;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
2738
3540
|
let hasUnsubmittedCommit = false;
|
|
2739
3541
|
let unsubmittedCommitGraceExpired = false;
|
|
2740
3542
|
if (now >= deadline) {
|
|
2741
3543
|
// Only reclaim after the commit has genuinely stalled. Submitted writes no longer depend on these
|
|
2742
3544
|
// separate snapshots; pre-submit handles can still carry writes needed by the attempt.
|
|
2743
3545
|
txn.releaseStalledCommitReadResources();
|
|
2744
|
-
if (!txn.isProtectedCommit()) {
|
|
3546
|
+
if (!loggedRetry && !txn.isProtectedCommit()) {
|
|
2745
3547
|
if (!root.postSubmitPoisoned)
|
|
2746
3548
|
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) {
|
|
3549
|
+
for (const link of collectCommitLinks(root, txn)) {
|
|
2759
3550
|
if (link.committing && !link.nativeCommitSubmitted) {
|
|
2760
3551
|
hasUnsubmittedCommit = true;
|
|
2761
3552
|
break;
|
|
@@ -2775,14 +3566,16 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
|
|
|
2775
3566
|
txn.open === exports.TRANSACTION_STATE.CLOSED &&
|
|
2776
3567
|
!(txn === root && submittedCommitStillInFlight(root)))
|
|
2777
3568
|
return hasUnsubmittedCommit && !unsubmittedCommitGraceExpired;
|
|
2778
|
-
if (!
|
|
3569
|
+
if (!loggedRetry &&
|
|
3570
|
+
!root.stalledCommitLogged &&
|
|
3571
|
+
now - lastStalledCommitErrorAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
|
|
2779
3572
|
lastStalledCommitErrorAt = now;
|
|
2780
3573
|
root.stalledCommitLogged = true;
|
|
2781
3574
|
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
3575
|
`on this transaction is rejected until it settles, and the worker may require a restart, from table: ${txn.db?.name + (url ? ' path: ' + url : '')}`);
|
|
2783
3576
|
}
|
|
2784
3577
|
}
|
|
2785
|
-
else if (now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
|
|
3578
|
+
else if (!loggedRetry && now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
|
|
2786
3579
|
lastStalledCommitWarningAt = now;
|
|
2787
3580
|
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
3581
|
}
|
|
@@ -2933,10 +3726,20 @@ function setMaxOutstandingTxnDuration(ms) {
|
|
|
2933
3726
|
MAX_OUTSTANDING_TXN_DURATION = ms;
|
|
2934
3727
|
return previous;
|
|
2935
3728
|
}
|
|
3729
|
+
/** Test seam: overrides the per-retry backoff cap and returns the previous value. */
|
|
3730
|
+
function setMaxRetryDelayMs(ms) {
|
|
3731
|
+
const previous = MAX_RETRY_DELAY_MS;
|
|
3732
|
+
MAX_RETRY_DELAY_MS = ms;
|
|
3733
|
+
return previous;
|
|
3734
|
+
}
|
|
2936
3735
|
/** Test seam: whether the monitor supervises this logical transaction for its writes. */
|
|
2937
3736
|
function isWriteSupervised(txn) {
|
|
2938
3737
|
return supervisedWriteRoots.has(txn);
|
|
2939
3738
|
}
|
|
3739
|
+
/** Test seam: returns the current open-transaction monitor interval. */
|
|
3740
|
+
function getTxnExpiration() {
|
|
3741
|
+
return txnExpiration;
|
|
3742
|
+
}
|
|
2940
3743
|
function setTxnExpiration(ms) {
|
|
2941
3744
|
clearInterval(timer);
|
|
2942
3745
|
txnExpiration = ms;
|