From f04902627be29b5adc733e625bb4bb70a43e3dec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:58:59 +0000 Subject: [PATCH 1/3] Bump body-parser from 1.20.5 to 1.20.6 Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.5 to 1.20.6. - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.5...1.20.6) --- updated-dependencies: - dependency-name: body-parser dependency-version: 1.20.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 27b9ce2ef..3e8522d1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3010,9 +3010,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", From 3f65abe3b59e50cab0df7d3dae404e714c180172 Mon Sep 17 00:00:00 2001 From: Akanksha Jain Date: Wed, 29 Jul 2026 12:40:19 +0530 Subject: [PATCH 2/3] Maintenance updates and API version enhancements and enhanced error handling for the Blob and Queue services (#2675) * Updated npm package fixes * Updated changelog * Bump morgan and refresh audit changelog note * Bump service API versions to 2026-06-06 and update changelog * Bump axios/esbuild and update constants * Fix #2672: Handle GC startup errors during server initialization Fixes race condition where GC error during startup would attempt to close server while still in 'Starting' state, resulting in 'Cannot close server in status Starting' error. Changes: - Modified BlobServer and QueueServer error handlers to wait for server to reach 'Running' state before attempting close - Added timeout logic (up to 5 seconds) to wait for server startup completion - Gracefully handles case where server cannot transition to Running state - Allows Azurite to recover when encountering errors processing legacy persisted data from previous versions Fixes: #2672 * Add comprehensive test suite for issue #2672 - Added 4 test files with 10 total test cases covering: * Unit tests for GC error handler state machine (3 tests) * Blob server startup error recovery scenarios (3 tests) * Upgrade regression tests (2 tests) * Queue server startup error recovery scenarios (2 tests) - Tests validate: * Fresh start scenarios without persisted data * Graceful error handling during GC initialization in Starting state * Concurrent startup operations * Data persistence across upgrade scenarios * Multi-container and multi-blob scenarios * Recovery from timeout conditions - All 777 tests passing (including 10 new tests) - No regressions in existing tests - Full TypeScript compilation clean --- ChangeLog.md | 15 +- package-lock.json | 484 +++++++------ package.json | 8 +- scripts/azurite-cli.sh | 0 src/azurite.ts | 59 +- src/blob/BlobServer.ts | 85 ++- src/blob/SqlBlobServer.ts | 49 +- src/blob/persistence/LokiBlobMetadataStore.ts | 191 +++-- src/blob/utils/constants.ts | 5 +- src/common/GCCriticalErrorCloseHelper.ts | 70 ++ src/queue/QueueServer.ts | 74 +- src/queue/utils/constants.ts | 5 +- src/table/utils/constants.ts | 5 +- tests/blob/sas.test.ts | 351 ++++++---- tests/blob/startupErrorRecovery.test.ts | 226 ++++++ tests/blob/unit/gcErrorHandler.unit.test.ts | 118 ++++ tests/blob/upgradeRegression.test.ts | 660 ++++++++++++++++++ tests/queue/queueSas.test.ts | 39 +- tests/queue/startupErrorRecovery.test.ts | 166 +++++ tests/table/auth/sas.test.ts | 53 ++ 20 files changed, 2110 insertions(+), 553 deletions(-) mode change 100644 => 100755 scripts/azurite-cli.sh create mode 100644 src/common/GCCriticalErrorCloseHelper.ts create mode 100644 tests/blob/startupErrorRecovery.test.ts create mode 100644 tests/blob/unit/gcErrorHandler.unit.test.ts create mode 100644 tests/blob/upgradeRegression.test.ts create mode 100644 tests/queue/startupErrorRecovery.test.ts diff --git a/ChangeLog.md b/ChangeLog.md index d06a5d2d5..291b2c7a6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,20 @@ ## Upcoming Release +General: + +- Applied npm audit fix to updates across multiple dependencies to address security vulnerabilities and maintenance updates. +- Bumped the default Blob, Queue, and Table service API version to `2026-06-06`. +- Added support for service API versions `2026-04-06` and `2026-02-06` for Blob, Queue, and Table endpoints. + +Blob: + +- Fixed issue #2672 startup failures with legacy persisted data by adding backward-compatible restore for persisted `contentMD5` formats. + +Queue: + +- Fixed issue #2672 startup race condition by avoiding GC-triggered `close()` calls while server status is `Starting`. + ## 2026.06 Version 3.36.0 General: @@ -22,7 +36,6 @@ General: - Fix Windows SEA executable build producing a corrupted/bad `.exe` by stripping the Authenticode signature from the copied Node.js binary before injecting the SEA blob via `postject`. - Replace the hardcoded user delegation signing key literal with a deterministic internal signing seed to avoid VS Code extension publish secret-scan blocks while preserving stable user delegation SAS behavior. - Blob: - Remove the default value for the `BlobSequenceNumber` parameter in the Swagger definition. diff --git a/package-lock.json b/package-lock.json index 3e8522d1d..a4b7f38de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,14 @@ "@azure/ms-rest-js": "^2.7.0", "applicationinsights": "^2.9.6", "args": "^5.0.1", - "axios": "^1.17.0", + "axios": "^1.18.1", "etag": "^1.8.1", "express": "^4.16.4", "fs-extra": "^11.1.1", "glob-to-regexp": "^0.4.1", "jsonwebtoken": "^9.0.0", "lokijs": "^1.5.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "multistream": "^2.1.1", "mysql2": "^3.10.1", "rimraf": "^3.0.2", @@ -69,7 +69,7 @@ "autorest": "^3.6.0", "cross-env": "^7.0.3", "cross-var": "^1.1.0", - "esbuild": "^0.25.5", + "esbuild": "^0.28.1", "eslint": "^8.35.0", "find-process": "^1.4.4", "husky": "^8.0.1", @@ -334,16 +334,6 @@ "version": "1.14.1", "license": "0BSD" }, - "node_modules/@azure/ms-rest-js/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@azure/ms-rest-js/node_modules/xml2js": { "version": "0.5.0", "license": "MIT", @@ -397,23 +387,27 @@ } }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { - "version": "1.0.0-beta.9", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0.tgz", + "integrity": "sha512-Y8rZOIMXQY/GwNRL+uLVuwIn9aEa/KnnggyYUmFxC1MigmRJCNH5NxMmxKSpddXF9SW6Z1ijRd6Pptd2A5OhGw==", "license": "MIT", "dependencies": { "@azure/core-tracing": "^1.2.0", "@azure/logger": "^1.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "tslib": "^2.7.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/core": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -527,9 +521,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -544,9 +538,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -561,9 +555,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -578,9 +572,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -595,9 +589,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -612,9 +606,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -629,9 +623,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -646,9 +640,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -663,9 +657,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -680,9 +674,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -697,9 +691,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -714,9 +708,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -731,9 +725,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -748,9 +742,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -765,9 +759,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -782,9 +776,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -799,7 +793,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -814,9 +810,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -831,9 +827,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -848,9 +844,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -865,9 +861,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -882,9 +878,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -899,9 +895,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -916,9 +912,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -933,9 +929,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -950,9 +946,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1160,7 +1156,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.200.0", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -1190,14 +1188,14 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.200.0", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "shimmer": "^1.2.1" + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1227,6 +1225,23 @@ "node": ">=14" } }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "1.30.1", "license": "Apache-2.0", @@ -1250,11 +1265,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-web": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.9.0.tgz", + "integrity": "sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", - "@opentelemetry/sdk-trace-base": "2.0.1" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1264,7 +1281,9 @@ } }, "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/core": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1277,10 +1296,12 @@ } }, "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/resources": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1291,11 +1312,45 @@ } }, "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", - "@opentelemetry/resources": "2.0.1", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1540,10 +1595,6 @@ "@types/send": "*" } }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "license": "MIT" - }, "node_modules/@types/stoppable": { "version": "1.1.3", "dev": true, @@ -1808,6 +1859,8 @@ }, "node_modules/acorn-import-attributes": { "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "license": "MIT", "peerDependencies": { "acorn": "^8" @@ -2057,9 +2110,9 @@ } }, "node_modules/axios": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", - "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", @@ -2081,16 +2134,16 @@ } }, "node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3297,7 +3350,9 @@ "license": "ISC" }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, "node_modules/cli-cursor": { @@ -4110,9 +4165,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4123,32 +4178,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -4844,13 +4899,15 @@ } }, "node_modules/form-data": { - "version": "2.5.5", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -5166,7 +5223,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5363,13 +5422,15 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.14.2", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" } }, "node_modules/imurmurhash": { @@ -5598,8 +5659,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6504,6 +6577,8 @@ }, "node_modules/module-details-from-path": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, "node_modules/moment": { @@ -6524,17 +6599,23 @@ } }, "node_modules/morgan": { - "version": "1.10.1", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", - "on-finished": "~2.3.0", + "on-finished": "~2.4.1", "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/morgan/node_modules/debug": { @@ -6548,16 +6629,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/mri": { "version": "1.1.4", "license": "MIT", @@ -6973,10 +7044,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -7420,33 +7487,16 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.5.2", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" + "module-details-from-path": "^1.0.3" }, "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, "node_modules/resolve-from": { @@ -7457,19 +7507,6 @@ "node": ">=4" } }, - "node_modules/resolve/node_modules/is-core-module": { - "version": "2.16.1", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "dev": true, @@ -7735,13 +7772,6 @@ "node": ">= 10.0.0" } }, - "node_modules/sequelize/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "dev": true, @@ -8077,16 +8107,6 @@ "node": ">=4" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -8518,6 +8538,16 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "dev": true, diff --git a/package.json b/package.json index 29497be40..5b765830e 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,14 @@ "@azure/ms-rest-js": "^2.7.0", "applicationinsights": "^2.9.6", "args": "^5.0.1", - "axios": "^1.17.0", + "axios": "^1.18.1", "etag": "^1.8.1", "express": "^4.16.4", "fs-extra": "^11.1.1", "glob-to-regexp": "^0.4.1", "jsonwebtoken": "^9.0.0", "lokijs": "^1.5.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "multistream": "^2.1.1", "mysql2": "^3.10.1", "rimraf": "^3.0.2", @@ -74,7 +74,7 @@ "autorest": "^3.6.0", "cross-env": "^7.0.3", "cross-var": "^1.1.0", - "esbuild": "^0.25.5", + "esbuild": "^0.28.1", "eslint": "^8.35.0", "find-process": "^1.4.4", "husky": "^8.0.1", @@ -354,4 +354,4 @@ "url": "https://github.com/azure/azurite/issues" }, "homepage": "https://github.com/azure/azurite#readme" -} \ No newline at end of file +} diff --git a/scripts/azurite-cli.sh b/scripts/azurite-cli.sh old mode 100644 new mode 100755 diff --git a/src/azurite.ts b/src/azurite.ts index e856c3e25..297b86083 100644 --- a/src/azurite.ts +++ b/src/azurite.ts @@ -25,6 +25,7 @@ import TableServer from "./table/TableServer"; import { DEFAULT_TABLE_LOKI_DB_PATH } from "./table/utils/constants"; import { setExtentMemoryLimit } from "./common/ConfigurationBase"; import { AzuriteTelemetryClient } from "./common/Telemetry"; +import { ServerStatus } from "./common/ServerBase"; // tslint:disable:no-console @@ -42,30 +43,50 @@ function shutdown( AzuriteTelemetryClient.TraceStopEvent(); - console.log(blobBeforeCloseMessage); - blobServer.close().then(() => { - console.log(blobAfterCloseMessage); - }); - - console.log(queueBeforeCloseMessage); - queueServer.close().then(() => { - console.log(queueAfterCloseMessage); - }); - - console.log(tableBeforeCloseMessage); - tableServer.close().then(() => { - console.log(tableAfterCloseMessage); - }); + const closeWithDiagnostics = ( + beforeMessage: string, + afterMessage: string, + server: { getStatus(): ServerStatus; close(): Promise } + ) => { + const statusBeforeClose = server.getStatus(); + console.log(beforeMessage); + server + .close() + .then(() => { + console.log(afterMessage); + }) + .catch((err) => { + const errorMsg = err instanceof Error ? err.message : String(err); + console.error( + `Shutdown close failed in status ${statusBeforeClose}: ${errorMsg}` + ); + }); + }; + + closeWithDiagnostics( + blobBeforeCloseMessage, + blobAfterCloseMessage, + blobServer + ); + closeWithDiagnostics( + queueBeforeCloseMessage, + queueAfterCloseMessage, + queueServer + ); + closeWithDiagnostics( + tableBeforeCloseMessage, + tableAfterCloseMessage, + tableServer + ); } /** * Entry for Azurite services. */ async function main() { - // Initialize and validate environment values from command line parameters const env = new Environment(); - + const location = await env.location(); await ensureDir(location); await access(location); @@ -105,7 +126,7 @@ async function main() { env.pwd(), env.oauth(), env.disableProductStyleUrl(), - env.inMemoryPersistence(), + env.inMemoryPersistence() ); const tableConfig = new TableConfiguration( @@ -124,7 +145,7 @@ async function main() { env.pwd(), env.oauth(), env.disableProductStyleUrl(), - env.inMemoryPersistence(), + env.inMemoryPersistence() ); // We use logger singleton as global debugger logger to track detailed outputs cross layers @@ -167,7 +188,7 @@ async function main() { console.log( `Azurite Table service is successfully listening at ${tableServer.getHttpServerAddress()}` ); - + AzuriteTelemetryClient.init(location, !env.disableTelemetry(), env); await AzuriteTelemetryClient.TraceStartEvent(); diff --git a/src/blob/BlobServer.ts b/src/blob/BlobServer.ts index 2f0e10dc3..fcf5952eb 100644 --- a/src/blob/BlobServer.ts +++ b/src/blob/BlobServer.ts @@ -9,10 +9,13 @@ import IGCManager from "../common/IGCManager"; import IRequestListenerFactory from "../common/IRequestListenerFactory"; import logger from "../common/Logger"; import FSExtentStore from "../common/persistence/FSExtentStore"; -import MemoryExtentStore, { SharedChunkStore } from "../common/persistence/MemoryExtentStore"; +import MemoryExtentStore, { + SharedChunkStore +} from "../common/persistence/MemoryExtentStore"; import IExtentMetadataStore from "../common/persistence/IExtentMetadataStore"; import IExtentStore from "../common/persistence/IExtentStore"; import LokiExtentMetadataStore from "../common/persistence/LokiExtentMetadataStore"; +import { handleGCCriticalErrorClose } from "../common/GCCriticalErrorCloseHelper"; import ServerBase, { ServerStatus } from "../common/ServerBase"; import BlobConfiguration from "./BlobConfiguration"; import BlobRequestListenerFactory from "./BlobRequestListenerFactory"; @@ -79,39 +82,43 @@ export default class BlobServer extends ServerBase implements ICleaner { configuration.isMemoryPersistence ); - const extentMetadataStore: IExtentMetadataStore = new LokiExtentMetadataStore( - configuration.extentDBPath, - configuration.isMemoryPersistence - ); - - const extentStore: IExtentStore = configuration.isMemoryPersistence ? new MemoryExtentStore( - "blob", - configuration.memoryStore ?? SharedChunkStore, - extentMetadataStore, - logger, - (sc, er, em, ri) => new StorageError(sc, er, em, ri) - ) : new FSExtentStore( - extentMetadataStore, - configuration.persistencePathArray, - logger - ); + const extentMetadataStore: IExtentMetadataStore = + new LokiExtentMetadataStore( + configuration.extentDBPath, + configuration.isMemoryPersistence + ); + + const extentStore: IExtentStore = configuration.isMemoryPersistence + ? new MemoryExtentStore( + "blob", + configuration.memoryStore ?? SharedChunkStore, + extentMetadataStore, + logger, + (sc, er, em, ri) => new StorageError(sc, er, em, ri) + ) + : new FSExtentStore( + extentMetadataStore, + configuration.persistencePathArray, + logger + ); const accountDataStore: IAccountDataStore = new AccountDataStore(logger); // We can also change the HTTP framework here by // creating a new XXXListenerFactory implementing IRequestListenerFactory interface // and replace the default Express based request listener - const requestListenerFactory: IRequestListenerFactory = new BlobRequestListenerFactory( - metadataStore, - extentStore, - accountDataStore, - configuration.enableAccessLog, // Access log includes every handled HTTP request - configuration.accessLogWriteStream, - configuration.loose, - configuration.skipApiVersionCheck, - configuration.getOAuthLevel(), - configuration.disableProductStyleUrl - ); + const requestListenerFactory: IRequestListenerFactory = + new BlobRequestListenerFactory( + metadataStore, + extentStore, + accountDataStore, + configuration.enableAccessLog, // Access log includes every handled HTTP request + configuration.accessLogWriteStream, + configuration.loose, + configuration.skipApiVersionCheck, + configuration.getOAuthLevel(), + configuration.disableProductStyleUrl + ); super(host, port, httpServer, requestListenerFactory, configuration); @@ -121,14 +128,26 @@ export default class BlobServer extends ServerBase implements ICleaner { metadataStore, extentMetadataStore, extentStore, - () => { + (err: Error) => { // tslint:disable-next-line:no-console console.log(BEFORE_CLOSE_MESSAGE_GC_ERROR); logger.info(BEFORE_CLOSE_MESSAGE_GC_ERROR); - this.close().then(() => { - // tslint:disable-next-line:no-console - console.log(AFTER_CLOSE_MESSAGE); - logger.info(AFTER_CLOSE_MESSAGE); + logger.error( + `Blob GC Manager critical error: ${err.message}\nStack: ${err.stack}` + ); + logger.warn( + "This may be due to: " + + "1) Persisted database format changes, " + + "2) File system permission issues, " + + "3) Corrupted metadata files. " + + "See error details above." + ); + + handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => this.status, + close: () => this.close(), + logger }); }, logger diff --git a/src/blob/SqlBlobServer.ts b/src/blob/SqlBlobServer.ts index c0e07e6d3..020a9b6a5 100644 --- a/src/blob/SqlBlobServer.ts +++ b/src/blob/SqlBlobServer.ts @@ -6,6 +6,7 @@ import { CertOptions } from "../common/ConfigurationBase"; import IAccountDataStore from "../common/IAccountDataStore"; import IGCManager from "../common/IGCManager"; import IRequestListenerFactory from "../common/IRequestListenerFactory"; +import { handleGCCriticalErrorClose } from "../common/GCCriticalErrorCloseHelper"; import logger from "../common/Logger"; import FSExtentStore from "../common/persistence/FSExtentStore"; import IExtentMetadataStore from "../common/persistence/IExtentMetadataStore"; @@ -69,12 +70,13 @@ export default class SqlBlobServer extends ServerBase { configuration.sequelizeOptions ); - const extentMetadataStore: IExtentMetadataStore = new SqlExtentMetadataStore( - // Currently, extent metadata and blob metadata share same database - // But they can use separate databases per future requirements - configuration.sqlURL, - configuration.sequelizeOptions - ); + const extentMetadataStore: IExtentMetadataStore = + new SqlExtentMetadataStore( + // Currently, extent metadata and blob metadata share same database + // But they can use separate databases per future requirements + configuration.sqlURL, + configuration.sequelizeOptions + ); const extentStore: IExtentStore = new FSExtentStore( extentMetadataStore, @@ -87,17 +89,18 @@ export default class SqlBlobServer extends ServerBase { // We can also change the HTTP framework here by // creating a new XXXListenerFactory implementing IRequestListenerFactory interface // and replace the default Express based request listener - const requestListenerFactory: IRequestListenerFactory = new BlobRequestListenerFactory( - metadataStore, - extentStore, - accountDataStore, - configuration.enableAccessLog, // Access log includes every handled HTTP request - configuration.accessLogWriteStream, - configuration.loose, - configuration.skipApiVersionCheck, - configuration.getOAuthLevel(), - configuration.disableProductStyleUrl - ); + const requestListenerFactory: IRequestListenerFactory = + new BlobRequestListenerFactory( + metadataStore, + extentStore, + accountDataStore, + configuration.enableAccessLog, // Access log includes every handled HTTP request + configuration.accessLogWriteStream, + configuration.loose, + configuration.skipApiVersionCheck, + configuration.getOAuthLevel(), + configuration.disableProductStyleUrl + ); super(host, port, httpServer, requestListenerFactory, configuration); @@ -107,16 +110,16 @@ export default class SqlBlobServer extends ServerBase { metadataStore, extentMetadataStore, extentStore, - error => { + (error) => { // tslint:disable-next-line:no-console console.log(BEFORE_CLOSE_MESSAGE_GC_ERROR, error); logger.info(BEFORE_CLOSE_MESSAGE_GC_ERROR + JSON.stringify(error)); - // TODO: Bring this back when GC based on SQL implemented - this.close().then(() => { - // tslint:disable-next-line:no-console - console.log(AFTER_CLOSE_MESSAGE); - logger.info(AFTER_CLOSE_MESSAGE); + handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => this.status, + close: () => this.close(), + logger }); }, logger diff --git a/src/blob/persistence/LokiBlobMetadataStore.ts b/src/blob/persistence/LokiBlobMetadataStore.ts index 7efc4d7c6..d0e3f62d6 100644 --- a/src/blob/persistence/LokiBlobMetadataStore.ts +++ b/src/blob/persistence/LokiBlobMetadataStore.ts @@ -65,7 +65,11 @@ import IBlobMetadataStore, { import PageWithDelimiter from "./PageWithDelimiter"; import FilterBlobPage from "./FilterBlobPage"; import { generateQueryBlobWithTagsWhereFunction } from "./QueryInterpreter/QueryInterpreter"; -import { getBlobTagsCount, getTagsFromString, toBlobTags } from "../utils/utils"; +import { + getBlobTagsCount, + getTagsFromString, + toBlobTags +} from "../utils/utils"; /** * This is a metadata source implementation for blob based on loki DB. @@ -95,7 +99,8 @@ import { getBlobTagsCount, getTagsFromString, toBlobTags } from "../utils/utils" * @class LokiBlobMetadataStore */ export default class LokiBlobMetadataStore - implements IBlobMetadataStore, IGCExtentProvider { + implements IBlobMetadataStore, IGCExtentProvider +{ private readonly db: Loki; private initialized: boolean = false; @@ -108,14 +113,22 @@ export default class LokiBlobMetadataStore private readonly pageBlobRangesManager = new PageBlobRangesManager(); - public constructor(public readonly lokiDBPath: string, inMemory: boolean) { - this.db = new Loki(lokiDBPath, inMemory ? { - persistenceMethod: "memory" - } : { - persistenceMethod: "fs", - autosave: true, - autosaveInterval: 5000 - }); + public constructor( + public readonly lokiDBPath: string, + inMemory: boolean + ) { + this.db = new Loki( + lokiDBPath, + inMemory + ? { + persistenceMethod: "memory" + } + : { + persistenceMethod: "fs", + autosave: true, + autosaveInterval: 5000 + } + ); } public isInitialized(): boolean { @@ -329,9 +342,9 @@ export default class LokiBlobMetadataStore prefix === "" ? { name: { $gt: marker }, accountName: account } : { - name: { $regex: `^${this.escapeRegex(prefix)}`, $gt: marker }, - accountName: account - }; + name: { $regex: `^${this.escapeRegex(prefix)}`, $gt: marker }, + accountName: account + }; // Workaround for loki which will ignore $gt when providing $regex const query2 = { name: { $gt: marker } }; @@ -753,10 +766,10 @@ export default class LokiBlobMetadataStore const leaseTimeSeconds: number = doc.properties.leaseState === Models.LeaseStateType.Breaking && - doc.leaseBreakTime + doc.leaseBreakTime ? Math.round( - (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 - ) + (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 + ) : 0; coll.update(doc); @@ -830,7 +843,7 @@ export default class LokiBlobMetadataStore container?: string, where?: string, maxResults: number = DEFAULT_LIST_BLOBS_MAX_RESULTS, - marker: string = "", + marker: string = "" ): Promise<[FilterBlobModel[], string | undefined]> { const query: any = {}; if (account !== undefined) { @@ -838,14 +851,13 @@ export default class LokiBlobMetadataStore } if (container !== undefined) { query.containerName = container; - await this.checkContainerExist( - context, - account, - container - ); + await this.checkContainerExist(context, account, container); } - const filterFunction = generateQueryBlobWithTagsWhereFunction(context, where!); + const filterFunction = generateQueryBlobWithTagsWhereFunction( + context, + where! + ); const coll = this.db.getCollection(this.BLOBS_COLLECTION); const page = new FilterBlobPage(maxResults); @@ -857,7 +869,7 @@ export default class LokiBlobMetadataStore return obj.name > marker!; }) .where((obj) => { - return obj.snapshot === undefined || obj.snapshot === ''; + return obj.snapshot === undefined || obj.snapshot === ""; }) .sort((obj1, obj2) => { if (obj1.name === obj2.name) return 0; @@ -868,22 +880,24 @@ export default class LokiBlobMetadataStore .limit(maxResults) .data(); - return doc.map((item) => { - let blobItem: FilterBlobModel; - blobItem = { - name: item.name, - containerName: item.containerName, - tags: item.blobTags - }; - return blobItem; - }).filter((blobItem) => { - const tagsMeetConditions = filterFunction(blobItem); - if (tagsMeetConditions.length !== 0) { - blobItem.tags = { blobTagSet: toBlobTags(tagsMeetConditions) }; - return true; - } - return false; - }); + return doc + .map((item) => { + let blobItem: FilterBlobModel; + blobItem = { + name: item.name, + containerName: item.containerName, + tags: item.blobTags + }; + return blobItem; + }) + .filter((blobItem) => { + const tagsMeetConditions = filterFunction(blobItem); + if (tagsMeetConditions.length !== 0) { + blobItem.tags = { blobTagSet: toBlobTags(tagsMeetConditions) }; + return true; + } + return false; + }); }; const nameItem = (item: FilterBlobModel) => { @@ -892,10 +906,7 @@ export default class LokiBlobMetadataStore const [blobItems, nextMarker] = await page.fill(readPage, nameItem); - return [ - blobItems, - nextMarker - ]; + return [blobItems, nextMarker]; } public async listBlobs( @@ -925,7 +936,11 @@ export default class LokiBlobMetadataStore } const coll = this.db.getCollection(this.BLOBS_COLLECTION); - const page = new PageWithDelimiter(maxResults, delimiter, prefix); + const page = new PageWithDelimiter( + maxResults, + delimiter, + prefix + ); const readPage = async (offset: number): Promise => { return await coll .chain() @@ -953,7 +968,10 @@ export default class LokiBlobMetadataStore return item.name; }; - const [blobItems, blobPrefixes, nextMarker] = await page.fill(readPage, nameItem); + const [blobItems, blobPrefixes, nextMarker] = await page.fill( + readPage, + nameItem + ); return [ blobItems.map((doc) => { @@ -1766,10 +1784,10 @@ export default class LokiBlobMetadataStore const leaseTimeSeconds: number = doc.properties.leaseState === Models.LeaseStateType.Breaking && - doc.leaseBreakTime + doc.leaseBreakTime ? Math.round( - (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 - ) + (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 + ) : 0; coll.update(doc); @@ -1934,8 +1952,10 @@ export default class LokiBlobMetadataStore throw StorageErrorFactory.getBlobNotFound(context.contextId!); } - if (sourceBlob.properties.accessTier === Models.AccessTier.Archive - && (tier === undefined || source.account !== destination.account)) { + if ( + sourceBlob.properties.accessTier === Models.AccessTier.Archive && + (tier === undefined || source.account !== destination.account) + ) { throw StorageErrorFactory.getBlobArchived(context.contextId!); } @@ -1981,7 +2001,9 @@ export default class LokiBlobMetadataStore remainingRetentionDays: undefined, archiveStatus: undefined, accessTierChangeTime: undefined, - ...(sourceBlob.properties.blobType === Models.BlobType.AppendBlob && { isSealed: options.sealBlob }), + ...(sourceBlob.properties.blobType === Models.BlobType.AppendBlob && { + isSealed: options.sealBlob + }) }, metadata: metadata === undefined || Object.keys(metadata).length === 0 @@ -2000,7 +2022,10 @@ export default class LokiBlobMetadataStore destBlob !== undefined ? destBlob.leaseBreakTime : undefined, committedBlocksInOrder: sourceBlob.committedBlocksInOrder, persistency: sourceBlob.persistency, - blobTags: options.blobTagsString === undefined ? undefined : getTagsFromString(options.blobTagsString, context.contextId!) + blobTags: + options.blobTagsString === undefined + ? undefined + : getTagsFromString(options.blobTagsString, context.contextId!) }; if ( @@ -2076,7 +2101,7 @@ export default class LokiBlobMetadataStore ifUnmodifiedSince: options.sourceModifiedAccessConditions.sourceIfUnmodifiedSince, ifMatch: options.sourceModifiedAccessConditions.sourceIfMatch, - ifNoneMatch: options.sourceModifiedAccessConditions.sourceIfNoneMatch, + ifNoneMatch: options.sourceModifiedAccessConditions.sourceIfNoneMatch // Storage service will ignore x-ms-source-if-tags header for copyFromUrl }, sourceBlob @@ -2188,9 +2213,12 @@ export default class LokiBlobMetadataStore destBlob !== undefined ? destBlob.leaseBreakTime : undefined, committedBlocksInOrder: sourceBlob.committedBlocksInOrder, persistency: sourceBlob.persistency, - blobTags: options.copySourceTags === Models.BlobCopySourceTags.COPY ? - sourceBlob.blobTags - : options.blobTagsString === undefined ? undefined : getTagsFromString(options.blobTagsString, context.contextId!) + blobTags: + options.copySourceTags === Models.BlobCopySourceTags.COPY + ? sourceBlob.blobTags + : options.blobTagsString === undefined + ? undefined + : getTagsFromString(options.blobTagsString, context.contextId!) }; if ( @@ -2281,7 +2309,9 @@ export default class LokiBlobMetadataStore // Archive -> Coo/Hot will return 202 if ( doc.properties.accessTier === Models.AccessTier.Archive && - (tier === Models.AccessTier.Cool || tier === Models.AccessTier.Hot || tier === Models.AccessTier.Cold) + (tier === Models.AccessTier.Cool || + tier === Models.AccessTier.Hot || + tier === Models.AccessTier.Cold) ) { responseCode = 202; } @@ -2366,7 +2396,7 @@ export default class LokiBlobMetadataStore const existBlockDoc = coll.findOne({ blobName: block.blobName, accountName: block.accountName, - containerName: block.containerName, + containerName: block.containerName }); if (existBlockDoc) { if ( @@ -2418,10 +2448,7 @@ export default class LokiBlobMetadataStore } const lease = new BlobLeaseAdapter(doc); - new BlobWriteLeaseValidator(leaseAccessConditions).validate( - lease, - context - ); + new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context); if (doc.properties.isSealed) { throw StorageErrorFactory.getBlobSealed(context.contextId); @@ -3109,7 +3136,7 @@ export default class LokiBlobMetadataStore * @memberof LokiBlobMetadataStore */ private restoreUint8Array(obj: any): Uint8Array | undefined { - if (typeof obj !== "object") { + if (obj === null || typeof obj !== "object") { return undefined; } @@ -3117,6 +3144,17 @@ export default class LokiBlobMetadataStore return new Uint8Array(obj); } + // Backward compatibility: persisted Buffer JSON shape from previous versions + // e.g. { type: "Buffer", data: [1,2,3] } + if (obj.type === "Buffer" && Array.isArray(obj.data)) { + return new Uint8Array(obj.data); + } + + // Backward compatibility: plain array-like persisted by serializers + if (Array.isArray(obj)) { + return new Uint8Array(obj); + } + const length = Object.keys(obj).length; const arr = Buffer.allocUnsafe(length); @@ -3517,22 +3555,22 @@ export default class LokiBlobMetadataStore } /** - * Seal blob. - * - * @param {Context} context - * @param {string} account - * @param {string} container - * @param {string} blob - * @returns {Promise} - * @memberof IBlobMetadataStore - */ + * Seal blob. + * + * @param {Context} context + * @param {string} account + * @param {string} container + * @param {string} blob + * @returns {Promise} + * @memberof IBlobMetadataStore + */ public async sealBlob( context: Context, account: string, container: string, blob: string, snapshot: string | undefined, - options: Models.AppendBlobSealOptionalParams, + options: Models.AppendBlobSealOptionalParams ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlob(context, account, container, blob); @@ -3548,7 +3586,10 @@ export default class LokiBlobMetadataStore } const lease = new BlobLeaseAdapter(doc); - new BlobWriteLeaseValidator(options.leaseAccessConditions).validate(lease, context); + new BlobWriteLeaseValidator(options.leaseAccessConditions).validate( + lease, + context + ); new BlobWriteLeaseSyncer(doc).sync(lease); doc.properties.isSealed = true; diff --git a/src/blob/utils/constants.ts b/src/blob/utils/constants.ts index 40319eea4..5a93a7dcd 100644 --- a/src/blob/utils/constants.ts +++ b/src/blob/utils/constants.ts @@ -2,7 +2,7 @@ import { StoreDestinationArray } from "../../common/persistence/IExtentStore"; import * as Models from "../generated/artifacts/models"; export const VERSION = "3.36.0"; -export const BLOB_API_VERSION = "2025-11-05"; +export const BLOB_API_VERSION = "2026-06-06"; export const DEFAULT_BLOB_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access export const DEFAULT_LIST_BLOBS_MAX_RESULTS = 5000; export const DEFAULT_LIST_CONTAINERS_MAX_RESULTS = 5000; @@ -98,6 +98,9 @@ export const DEFAULT_BLOB_PERSISTENCE_ARRAY: StoreDestinationArray = [ ]; export const ValidAPIVersions = [ + "2026-06-06", + "2026-04-06", + "2026-02-06", "2025-11-05", "2025-07-05", "2025-05-05", diff --git a/src/common/GCCriticalErrorCloseHelper.ts b/src/common/GCCriticalErrorCloseHelper.ts new file mode 100644 index 000000000..6b7112d32 --- /dev/null +++ b/src/common/GCCriticalErrorCloseHelper.ts @@ -0,0 +1,70 @@ +import ILogger from "./ILogger"; +import { ServerStatus } from "./ServerBase"; + +export interface GCCriticalErrorCloseHelperOptions { + readonly serviceName: string; + readonly getStatus: () => ServerStatus; + readonly close: () => Promise; + readonly logger: ILogger; + readonly waitIntervalMs?: number; + readonly maxAttempts?: number; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => { + const timer = setTimeout(resolve, ms) as unknown as { + unref?: () => void; + }; + if (typeof timer.unref === "function") { + timer.unref(); + } + }); +} + +/** + * Handles the startup/close race when a critical GC error occurs while a server is still Starting. + */ +export async function handleGCCriticalErrorClose( + options: GCCriticalErrorCloseHelperOptions +): Promise { + const waitIntervalMs = options.waitIntervalMs ?? 100; + const maxAttempts = options.maxAttempts ?? 50; + + try { + let attempts = 0; + let startupTimeoutLogged = false; + while (options.getStatus() === ServerStatus.Starting) { + if (!startupTimeoutLogged && attempts >= maxAttempts) { + startupTimeoutLogged = true; + options.logger.error( + `GC error occurred during server startup. Server did not transition to Running state ` + + `within ~${waitIntervalMs * maxAttempts}ms. Current status: ${ServerStatus.Starting}. ` + + `Will keep waiting and close if server reaches Running later.` + ); + } + + await delay(waitIntervalMs); + attempts++; + } + + const status = options.getStatus(); + if (status === ServerStatus.Running) { + options.logger.info( + `Shutting down ${options.serviceName} server due to GC critical error` + ); + await options.close(); + return; + } + + options.logger.warn( + `${options.serviceName} server status is ${status} (expected Running). ` + + `Server state may be inconsistent. Check logs for root cause.` + ); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + options.logger.error( + `${options.serviceName} server error during GC error handling: ${errorMsg}. ` + + `Server status: ${options.getStatus()}` + ); + } +} diff --git a/src/queue/QueueServer.ts b/src/queue/QueueServer.ts index cc61bd200..802007082 100644 --- a/src/queue/QueueServer.ts +++ b/src/queue/QueueServer.ts @@ -8,10 +8,13 @@ import IGCManager from "../common/IGCManager"; import IRequestListenerFactory from "../common/IRequestListenerFactory"; import logger from "../common/Logger"; import FSExtentStore from "../common/persistence/FSExtentStore"; -import MemoryExtentStore, { SharedChunkStore } from "../common/persistence/MemoryExtentStore"; +import MemoryExtentStore, { + SharedChunkStore +} from "../common/persistence/MemoryExtentStore"; import IExtentMetadataStore from "../common/persistence/IExtentMetadataStore"; import IExtentStore from "../common/persistence/IExtentStore"; import LokiExtentMetadataStore from "../common/persistence/LokiExtentMetadataStore"; +import { handleGCCriticalErrorClose } from "../common/GCCriticalErrorCloseHelper"; import ServerBase, { ServerStatus } from "../common/ServerBase"; import QueueGCManager from "./gc/QueueGCManager"; import IQueueMetadataStore from "./persistence/IQueueMetadataStore"; @@ -71,7 +74,7 @@ export default class QueueServer extends ServerBase { } if (configuration.keepAliveTimeout > 0) { - httpServer.keepAliveTimeout = configuration.keepAliveTimeout * 1000 + httpServer.keepAliveTimeout = configuration.keepAliveTimeout * 1000; } // We can change the persistency layer implementation by @@ -87,33 +90,36 @@ export default class QueueServer extends ServerBase { configuration.isMemoryPersistence ); - const extentStore: IExtentStore = configuration.isMemoryPersistence ? new MemoryExtentStore( - "queue", - configuration.memoryStore ?? SharedChunkStore, - extentMetadataStore, - logger, - (sc, er, em, ri) => new StorageError(sc, er, em, ri) - ) : new FSExtentStore( - extentMetadataStore, - configuration.persistencePathArray, - logger - ); + const extentStore: IExtentStore = configuration.isMemoryPersistence + ? new MemoryExtentStore( + "queue", + configuration.memoryStore ?? SharedChunkStore, + extentMetadataStore, + logger, + (sc, er, em, ri) => new StorageError(sc, er, em, ri) + ) + : new FSExtentStore( + extentMetadataStore, + configuration.persistencePathArray, + logger + ); const accountDataStore: IAccountDataStore = new AccountDataStore(logger); // We can also change the HTTP framework here by // creating a new XXXListenerFactory implementing IRequestListenerFactory interface // and replace the default Express based request listener - const requestListenerFactory: IRequestListenerFactory = new QueueRequestListenerFactory( - metadataStore, - extentStore, - accountDataStore, - configuration.enableAccessLog, // Access log includes every handled HTTP request - configuration.accessLogWriteStream, - configuration.skipApiVersionCheck, - configuration.getOAuthLevel(), - configuration.disableProductStyleUrl - ); + const requestListenerFactory: IRequestListenerFactory = + new QueueRequestListenerFactory( + metadataStore, + extentStore, + accountDataStore, + configuration.enableAccessLog, // Access log includes every handled HTTP request + configuration.accessLogWriteStream, + configuration.skipApiVersionCheck, + configuration.getOAuthLevel(), + configuration.disableProductStyleUrl + ); super(host, port, httpServer, requestListenerFactory, configuration); @@ -121,14 +127,26 @@ export default class QueueServer extends ServerBase { metadataStore, extentMetadataStore, extentStore, - () => { + (err: Error) => { // tslint:disable-next-line:no-console console.log(BEFORE_CLOSE_MESSAGE_GC_ERROR); logger.info(BEFORE_CLOSE_MESSAGE_GC_ERROR); - this.close().then(() => { - // tslint:disable-next-line:no-console - console.log(AFTER_CLOSE_MESSAGE); - logger.info(AFTER_CLOSE_MESSAGE); + logger.error( + `Queue GC Manager critical error: ${err.message}\nStack: ${err.stack}` + ); + logger.warn( + "This may be due to: " + + "1) Persisted database format changes, " + + "2) File system permission issues, " + + "3) Corrupted metadata files. " + + "See error details above." + ); + + handleGCCriticalErrorClose({ + serviceName: "Queue", + getStatus: () => this.status, + close: () => this.close(), + logger }); }, logger diff --git a/src/queue/utils/constants.ts b/src/queue/utils/constants.ts index 950bd2a70..53033a172 100644 --- a/src/queue/utils/constants.ts +++ b/src/queue/utils/constants.ts @@ -1,7 +1,7 @@ import { StoreDestinationArray } from "../../common/persistence/IExtentStore"; export const VERSION = "3.36.0"; -export const QUEUE_API_VERSION = "2025-11-05"; +export const QUEUE_API_VERSION = "2026-06-06"; export const DEFAULT_QUEUE_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access export const DEFAULT_QUEUE_LISTENING_PORT = 10001; export const IS_PRODUCTION = process.env.NODE_ENV === "production"; @@ -91,6 +91,9 @@ export const DEFAULT_QUEUE_PERSISTENCE_ARRAY: StoreDestinationArray = [ ]; export const ValidAPIVersions = [ + "2026-06-06", + "2026-04-06", + "2026-02-06", "2025-11-05", "2025-07-05", "2025-05-05", diff --git a/src/table/utils/constants.ts b/src/table/utils/constants.ts index a6e6174ae..4ecdfb303 100644 --- a/src/table/utils/constants.ts +++ b/src/table/utils/constants.ts @@ -18,7 +18,7 @@ export enum TABLE_STATUSCODE { } export const DEFAULT_TABLE_CONTEXT_PATH = "azurite_table_context"; -export const TABLE_API_VERSION = "2025-11-05"; +export const TABLE_API_VERSION = "2026-06-06"; export const VERSION = "3.36.0"; // Max Body size is 4 MB export const BODY_SIZE_MAX = 1024 * 1024 * 4; @@ -74,6 +74,9 @@ export const DEFAULT_TABLE_PERSISTENCE_ARRAY: StoreDestinationArray = [ export const QUERY_RESULT_MAX_NUM = 1000; export const ValidAPIVersions = [ + "2026-06-06", + "2026-04-06", + "2026-02-06", "2025-11-05", "2025-07-05", "2025-05-05", diff --git a/tests/blob/sas.test.ts b/tests/blob/sas.test.ts index 3385324d3..d3294a12c 100644 --- a/tests/blob/sas.test.ts +++ b/tests/blob/sas.test.ts @@ -40,9 +40,8 @@ configLogger(false); describe("Shared Access Signature (SAS) authentication", () => { // Setup two accounts for validating cross-account copy operations - process.env[ - "AZURITE_ACCOUNTS" - ] = `${EMULATOR_ACCOUNT_NAME}:${EMULATOR_ACCOUNT_KEY_STR}:${EMULATOR_ACCOUNT_KEY2_STR};${EMULATOR_ACCOUNT2_NAME}:${EMULATOR_ACCOUNT2_KEY_STR}`; + process.env["AZURITE_ACCOUNTS"] = + `${EMULATOR_ACCOUNT_NAME}:${EMULATOR_ACCOUNT_KEY_STR}:${EMULATOR_ACCOUNT_KEY2_STR};${EMULATOR_ACCOUNT2_NAME}:${EMULATOR_ACCOUNT2_KEY_STR}`; const factory = new BlobTestServerFactory(); const server = factory.createServer(); @@ -81,10 +80,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const serviceClientInvalid = new BlobServiceClient( baseURL, newPipeline( - new StorageSharedKeyCredential( - EMULATOR_ACCOUNT_NAME, - "invalidKey" - ), + new StorageSharedKeyCredential(EMULATOR_ACCOUNT_NAME, "invalidKey"), { retryOptions: { maxTries: 1 }, // Make sure socket is closed once the operation is done. @@ -119,11 +115,11 @@ describe("Shared Access Signature (SAS) authentication", () => { }); it("generateAccountSASQueryParameters should generate correct hashes", async () => { - const startDate = new Date(Date.UTC(2022, 3, 16, 6, 31, 48, 0)); const endDate = new Date(Date.UTC(2022, 3, 17, 6, 31, 48, 0)); - const storageSharedKeyCredential = (serviceClient as any).credential as StorageSharedKeyCredential; + const storageSharedKeyCredential = (serviceClient as any) + .credential as StorageSharedKeyCredential; const sas = generateAccountSASQueryParameters( { @@ -139,7 +135,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ).toString(); - assert.equal(sas, "sv=2016-05-31&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=bTjnB%2ByT4DkYQcmMfpedew72%2FKdvTMuz29uatYuWYME%3D"); + assert.equal( + sas, + "sv=2016-05-31&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=bTjnB%2ByT4DkYQcmMfpedew72%2FKdvTMuz29uatYuWYME%3D" + ); const sas2 = generateAccountSASQueryParameters( { @@ -155,7 +154,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ).toString(); - assert.equal(sas2, "sv=2018-11-09&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=zty6%2BwJgp86CuAlzq9w%2F%2BavVwjoEHgBOTXXmN7xAyio%3D"); + assert.equal( + sas2, + "sv=2018-11-09&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=zty6%2BwJgp86CuAlzq9w%2F%2BavVwjoEHgBOTXXmN7xAyio%3D" + ); const sas3 = generateAccountSASQueryParameters( { @@ -171,7 +173,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ).toString(); - assert.equal(sas3, "sv=2020-12-06&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=QB5jKCJTqn7jcPidkkXAazBQBL6VDMY7HumUIM78ROE%3D"); + assert.equal( + sas3, + "sv=2020-12-06&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=QB5jKCJTqn7jcPidkkXAazBQBL6VDMY7HumUIM78ROE%3D" + ); }); it("generateAccountSASQueryParameters should work @loki @sql", async () => { @@ -362,6 +367,45 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.ok(error); }); + it("generateAccountSASQueryParameters should reject duplicate SAS signature query @loki @sql", async () => { + const tmr = new Date(); + tmr.setDate(tmr.getDate() + 1); + + const storageSharedKeyCredential = (serviceClient as any).credential; + + const sas = generateAccountSASQueryParameters( + { + expiresOn: tmr, + permissions: AccountSASPermissions.parse("rwdlacup"), + resourceTypes: AccountSASResourceTypes.parse("sco").toString(), + services: AccountSASServices.parse("btqf").toString(), + protocol: SASProtocol.HttpsAndHttp, + version: "2016-05-31" + }, + storageSharedKeyCredential as StorageSharedKeyCredential + ).toString(); + + // Duplicate a critical auth field to emulate ambiguous query parsing cases. + const sasWithDuplicateSig = `${sas}&sig=duplicated-signature`; + const sasURL = `${serviceClient.url}?${sasWithDuplicateSig}`; + const serviceClientWithSAS = new BlobServiceClient( + sasURL, + newPipeline(new AnonymousCredential(), { + keepAliveOptions: { enable: false } + }) + ); + + let error; + try { + await serviceClientWithSAS.getProperties(); + } catch (err) { + error = err; + } + + assert.ok(error); + assert.deepStrictEqual(error.statusCode, 403); + }); + it("Synchronized copy blob should work with write permission in account SAS to override an existing blob @loki", async () => { const tmr = new Date(); tmr.setDate(tmr.getDate() + 1); @@ -391,9 +435,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -437,9 +480,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -490,9 +532,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -535,9 +576,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -581,9 +621,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -635,9 +674,8 @@ describe("Shared Access Signature (SAS) authentication", () => { const containerName = getUniqueName("con"); const containerClient = serviceClient.getContainerClient(containerName); - const containerClientWithSAS = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClientWithSAS = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -731,7 +769,9 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.deepStrictEqual(err.statusCode, 403); } - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${containerSAS}`); + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${containerSAS}` + ); try { await serviceClientWithSAS.getAccountInfo(); @@ -792,7 +832,8 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.deepStrictEqual(err.statusCode, 403); } - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${blobSAS}`, + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${blobSAS}`, newPipeline(new AnonymousCredential()) ); @@ -835,8 +876,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ); - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${blobSAS}`, - new AnonymousCredential()); + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${blobSAS}`, + new AnonymousCredential() + ); const blobBatchClient = serviceClientWithSAS.getBlobBatchClient(); // Assemble batch delete request. @@ -881,8 +924,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ); - const containerClientWithSAS = new ContainerClient(`${serviceClient.url}?${blobSAS}`, - new AnonymousCredential()); + const containerClientWithSAS = new ContainerClient( + `${serviceClient.url}?${blobSAS}`, + new AnonymousCredential() + ); const blobBatchClient = containerClientWithSAS.getBlobBatchClient(); // Assemble batch delete request. @@ -932,7 +977,7 @@ describe("Shared Access Signature (SAS) authentication", () => { let error; try { - await containerClientWithSAS.getBlobClient('blob').deleteIfExists(); + await containerClientWithSAS.getBlobClient("blob").deleteIfExists(); } catch (err) { error = err; } finally { @@ -953,7 +998,8 @@ describe("Shared Access Signature (SAS) authentication", () => { const tmr = new Date(); tmr.setDate(tmr.getDate() + 1); - const storageSharedKeyCredential = (serviceClientSecondKey as any).credential; + const storageSharedKeyCredential = (serviceClientSecondKey as any) + .credential; const containerName = getUniqueName("container"); const containerClient = serviceClient.getContainerClient(containerName); @@ -978,7 +1024,7 @@ describe("Shared Access Signature (SAS) authentication", () => { newPipeline(new AnonymousCredential()) ); - await containerClientWithSAS.getBlobClient('blob').deleteIfExists(); + await containerClientWithSAS.getBlobClient("blob").deleteIfExists(); await containerClient.delete(); }); @@ -1038,7 +1084,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-original"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-original"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-original" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-original"); assert.equal(downloadResponse.contentLanguage, "content-language-original"); assert.equal(downloadResponse.contentType, "content-type-original"); @@ -1059,7 +1108,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const containerClient = serviceClient.getContainerClient(containerName); await containerClient.create(); - const blobName = "this is a test file Ж 大 仮.jpg"; //filename contains spaces and special characters + const blobName = "this is a test file Ж 大 仮.jpg"; //filename contains spaces and special characters const blobClient = containerClient.getPageBlobClient(blobName); await blobClient.create(1024, { blobHTTPHeaders: { @@ -1098,14 +1147,20 @@ describe("Shared Access Signature (SAS) authentication", () => { const properties = await blobClientWithSAS.getProperties(); assert.equal(properties.cacheControl, "cache-control-original"); - assert.equal(properties.contentDisposition, `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}`); + assert.equal( + properties.contentDisposition, + `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}` + ); assert.equal(properties.contentEncoding, "content-encoding-original"); assert.equal(properties.contentLanguage, "content-language-original"); assert.equal(properties.contentType, "content-type-original"); const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-original"); - assert.equal(downloadResponse.contentDisposition, `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}`); + assert.equal( + downloadResponse.contentDisposition, + `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}` + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-original"); assert.equal(downloadResponse.contentLanguage, "content-language-original"); assert.equal(downloadResponse.contentType, "content-type-original"); @@ -1170,7 +1225,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1234,7 +1292,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-original"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-original"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-original" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-original"); assert.equal(downloadResponse.contentLanguage, "content-language-original"); assert.equal(downloadResponse.contentType, "content-type-original"); @@ -1299,7 +1360,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1368,7 +1432,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1748,8 +1815,9 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ); - const sasURL = `${blobClient.withSnapshot(response.snapshot!).url - }&${blobSAS}`; + const sasURL = `${ + blobClient.withSnapshot(response.snapshot!).url + }&${blobSAS}`; const blobClientWithSAS = new PageBlobClient( sasURL, newPipeline(new AnonymousCredential()) @@ -1765,7 +1833,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1783,12 +1854,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -1841,12 +1910,10 @@ describe("Shared Access Signature (SAS) authentication", () => { tmr.setDate(tmr.getDate() + 1); const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -1876,12 +1943,10 @@ describe("Shared Access Signature (SAS) authentication", () => { tmr.setDate(tmr.getDate() + 1); const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create({ access: "blob" }); @@ -1907,12 +1972,10 @@ describe("Shared Access Signature (SAS) authentication", () => { tmr.setDate(tmr.getDate() + 1); const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create({ access: "container" }); @@ -1940,12 +2003,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -1983,9 +2044,8 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.equal(properties.metadata!["foo"], "1"); assert.equal(properties.metadata!["bar"], "2"); - const targetBlobWithProps = targetContainerClient.getBlockBlobClient( - blobName2 - ); + const targetBlobWithProps = + targetContainerClient.getBlockBlobClient(blobName2); const operation2 = await targetBlobWithProps.beginCopyFromURL( `${sourceBlob.url}?${sas}`, { @@ -2013,12 +2073,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -2064,12 +2122,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -2099,9 +2155,8 @@ describe("Shared Access Signature (SAS) authentication", () => { // Copy From URI const targetBlob = targetContainerClient.getBlockBlobClient(blobName); - const targetBlobWithProps = targetContainerClient.getBlockBlobClient( - blobName2 - ); + const targetBlobWithProps = + targetContainerClient.getBlockBlobClient(blobName2); const copyResponse3 = await targetBlob.syncCopyFromURL( `${sourceBlob.url}?${sas}` ); @@ -2136,7 +2191,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlockName = getUniqueName("blockblob"); @@ -2146,16 +2201,18 @@ describe("Shared Access Signature (SAS) authentication", () => { const sasURL = await containerClient.generateSasUrl({ expiresOn: tmr, permissions: ContainerSASPermissions.parse("f"), - protocol: SASProtocol.HttpsAndHttp, + protocol: SASProtocol.HttpsAndHttp }); const containerClientWithSAS = new ContainerClient(sasURL); const expectedTags1: Tags = { - tag1: "val1", + tag1: "val1" }; - for await (const blob of containerClientWithSAS.findBlobsByTags(`tag1='val1'`)) { + for await (const blob of containerClientWithSAS.findBlobsByTags( + `tag1='val1'` + )) { assert.deepStrictEqual(blob.name, blockBlockName); assert.deepStrictEqual(blob.tags, expectedTags1); assert.deepStrictEqual(blob.tagValue, "val1"); @@ -2174,7 +2231,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2183,22 +2240,28 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; - const sasURL = generateAccountSASQueryParameters({ - expiresOn: tmr, - services: 'b', - resourceTypes: 'so', - permissions: AccountSASPermissions.parse("f"), - protocol: SASProtocol.HttpsAndHttp, - }, - sourceStorageSharedKeyCredential).toString(); + const sasURL = generateAccountSASQueryParameters( + { + expiresOn: tmr, + services: "b", + resourceTypes: "so", + permissions: AccountSASPermissions.parse("f"), + protocol: SASProtocol.HttpsAndHttp + }, + sourceStorageSharedKeyCredential + ).toString(); - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${sasURL}`); + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${sasURL}` + ); const expectedTags1: Tags = { - tag1: "val1", + tag1: "val1" }; - for await (const blob of serviceClientWithSAS.findBlobsByTags(`tag1='val1'`)) { + for await (const blob of serviceClientWithSAS.findBlobsByTags( + `tag1='val1'` + )) { assert.deepStrictEqual(blob.tags, expectedTags1); assert.deepStrictEqual(blob.tagValue, "val1"); } @@ -2216,7 +2279,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2225,22 +2288,28 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; - const sasURL = generateAccountSASQueryParameters({ - expiresOn: tmr, - services: 'b', - resourceTypes: 'c', - permissions: AccountSASPermissions.parse("f"), - protocol: SASProtocol.HttpsAndHttp, - }, - sourceStorageSharedKeyCredential).toString(); + const sasURL = generateAccountSASQueryParameters( + { + expiresOn: tmr, + services: "b", + resourceTypes: "c", + permissions: AccountSASPermissions.parse("f"), + protocol: SASProtocol.HttpsAndHttp + }, + sourceStorageSharedKeyCredential + ).toString(); - const containerClientWithSas = new ContainerClient(`${containerClient.url}?${sasURL}`); + const containerClientWithSas = new ContainerClient( + `${containerClient.url}?${sasURL}` + ); const expectedTags1: Tags = { - tag1: "val1", + tag1: "val1" }; - for await (const blob of containerClientWithSas.findBlobsByTags(`tag1='val1'`)) { + for await (const blob of containerClientWithSas.findBlobsByTags( + `tag1='val1'` + )) { assert.deepStrictEqual(blob.tags, expectedTags1); assert.deepStrictEqual(blob.tagValue, "val1"); } @@ -2258,7 +2327,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2268,7 +2337,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const sasURL = await blockBlobClient.generateSasUrl({ expiresOn: tmr, permissions: BlobSASPermissions.parse("t"), - protocol: SASProtocol.HttpsAndHttp, + protocol: SASProtocol.HttpsAndHttp }); const blobClientWithSAS = new BlobClient(sasURL); @@ -2290,7 +2359,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2299,16 +2368,20 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; - const sasURL = generateAccountSASQueryParameters({ - expiresOn: tmr, - services: 'b', - resourceTypes: 'o', - permissions: AccountSASPermissions.parse("t"), - protocol: SASProtocol.HttpsAndHttp, - }, - sourceStorageSharedKeyCredential).toString(); - - const blobClientWithSAS = new BlobClient(`${blockBlobClient.url}?${sasURL}`); + const sasURL = generateAccountSASQueryParameters( + { + expiresOn: tmr, + services: "b", + resourceTypes: "o", + permissions: AccountSASPermissions.parse("t"), + protocol: SASProtocol.HttpsAndHttp + }, + sourceStorageSharedKeyCredential + ).toString(); + + const blobClientWithSAS = new BlobClient( + `${blockBlobClient.url}?${sasURL}` + ); await blobClientWithSAS.setTags(tags); const getTagsResult = await blobClientWithSAS.getTags(); diff --git a/tests/blob/startupErrorRecovery.test.ts b/tests/blob/startupErrorRecovery.test.ts new file mode 100644 index 000000000..d153ace82 --- /dev/null +++ b/tests/blob/startupErrorRecovery.test.ts @@ -0,0 +1,226 @@ +import * as assert from "assert"; +import * as fs from "fs-extra"; + +import BlobConfiguration from "../../src/blob/BlobConfiguration"; +import BlobServer from "../../src/blob/BlobServer"; +import { configLogger } from "../../src/common/Logger"; +import { DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT } from "../../src/blob/utils/constants"; +import { ServerStatus } from "../../src/common/ServerBase"; + +// Set true to enable debug log +configLogger(false); + +describe("Blob Server Startup Error Recovery - Issue #2672 @loki", () => { + const testDbPath = "__test_startup_error_db_blob__.json"; + const testDbExtentPath = "__test_startup_error_db_blob_extent__.json"; + const blobStoragePath = "__test_startup_error_blobstorage__"; + + async function startWithTimeout(server: BlobServer): Promise { + let timeout: ReturnType | undefined; + + try { + await Promise.race([ + server.start(), + new Promise((_, reject) => { + timeout = setTimeout(() => { + reject(new Error("Server startup timeout after 10 seconds")); + }, 10000); + + const nodeTimer = timeout as unknown as { unref?: () => void }; + if (typeof nodeTimer.unref === "function") { + nodeTimer.unref(); + } + }) + ]); + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } + } + + after(async () => { + // Clean up test artifacts + if (fs.existsSync(testDbPath)) { + fs.removeSync(testDbPath); + } + if (fs.existsSync(testDbExtentPath)) { + fs.removeSync(testDbExtentPath); + } + if (fs.existsSync(blobStoragePath)) { + fs.removeSync(blobStoragePath); + } + }); + + it("should start successfully when no persisted data exists (fresh start)", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, blobStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + const config = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: blobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new BlobServer(config); + + try { + // Should start without errors + await server.start(); + assert.strictEqual(server.getStatus(), ServerStatus.Running); + + await server.close(); + assert.strictEqual(server.getStatus(), ServerStatus.Closed); + } finally { + await server.clean(); + } + }); + + it("should handle GC startup errors gracefully without throwing 'Cannot close server in status Starting'", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, blobStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + // Create a corrupted metadata file to simulate legacy data that causes GC errors + // This simulates loading data from 3.35.0 that causes compatibility issues + const corruptedMetadata = { + schemaVersion: "3.35.0", + data: { + collections: [ + { + name: "containers", + binaryIndices: [{ name: "name", keyType: "string" }], + data: [ + { + // Minimal container entry - may cause issues during GC init + name: "test-container", + meta: { revision: 0, created: 1600000000000, version: 0 } + } + ] + } + ] + } + }; + + // Write corrupted metadata to simulate legacy data + fs.writeFileSync(testDbPath, JSON.stringify(corruptedMetadata, null, 2)); + + const config = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: blobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new BlobServer(config); + try { + // Attempt to start server + await startWithTimeout(server); + + // If we get here, startup completed and should no longer be stuck in Starting. + assert.notStrictEqual( + server.getStatus(), + ServerStatus.Starting, + "Server should not remain in Starting state after startup completes" + ); + + // Try to close gracefully + if (server.getStatus() === ServerStatus.Running) { + await server.close(); + } + } catch (err) { + if (!(err instanceof Error)) { + throw err; + } + + if (err.message.includes("Cannot close server in status Starting")) { + assert.fail( + `Bug not fixed: Server attempted to close while in Starting state. Error: ${err.message}` + ); + } + + // Do not swallow startup failures such as timeout/hang. + throw err; + } finally { + try { + await server.clean(); + } catch { + // Cleanup may fail if startup failed, that's ok + } + } + }); + + it("should successfully recover from concurrent startup and GC initialization", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, blobStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + const config = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: blobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new BlobServer(config); + + try { + // Start the server - GC manager will initialize concurrently during startup + await server.start(); + + // Verify server reached Running state + assert.strictEqual( + server.getStatus(), + ServerStatus.Running, + "Server should be in Running status after successful start" + ); + + // Give GC some time to run + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Close should work without issues + await server.close(); + assert.strictEqual(server.getStatus(), ServerStatus.Closed); + } finally { + await server.clean(); + } + }); +}); diff --git a/tests/blob/unit/gcErrorHandler.unit.test.ts b/tests/blob/unit/gcErrorHandler.unit.test.ts new file mode 100644 index 000000000..c61cb6941 --- /dev/null +++ b/tests/blob/unit/gcErrorHandler.unit.test.ts @@ -0,0 +1,118 @@ +import * as assert from "assert"; + +import { handleGCCriticalErrorClose } from "../../../src/common/GCCriticalErrorCloseHelper"; +import ILogger from "../../../src/common/ILogger"; +import { ServerStatus } from "../../../src/common/ServerBase"; + +describe("GC Error Handler Helper Unit Tests - Issue #2672 @loki", () => { + function createTestLogger(events: string[]): ILogger { + return { + error: (message: string) => events.push(`error:${message}`), + warn: (message: string) => events.push(`warn:${message}`), + info: (message: string) => events.push(`info:${message}`), + verbose: () => { + // no-op for these tests + }, + debug: () => { + // no-op for these tests + } + }; + } + + it("waits for Running status, then closes", async () => { + let serverStatus: ServerStatus = ServerStatus.Starting; + let closeCalled = false; + const events: string[] = []; + + const transitionToRunning = new Promise((resolve) => { + setTimeout(() => { + serverStatus = ServerStatus.Running; + events.push("transitioned_to_running"); + resolve(); + }, 20); + }); + + await Promise.all([ + transitionToRunning, + handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => serverStatus, + close: async () => { + closeCalled = true; + events.push("close_called"); + }, + logger: createTestLogger(events), + waitIntervalMs: 5, + maxAttempts: 20 + }) + ]); + + assert.strictEqual(closeCalled, true, "close should be called"); + assert.ok( + events.some((e) => + e.includes("Shutting down Blob server due to GC critical error") + ), + "should log shutdown intent when status becomes Running" + ); + }); + + it("does not close if server transitions from Starting to Closed", async () => { + let serverStatus: ServerStatus = ServerStatus.Starting; + let closeCalled = false; + const events: string[] = []; + + // Ensure this happens well after maxAttempts * waitIntervalMs so timeout logging is deterministic. + setTimeout(() => { + serverStatus = ServerStatus.Closed; + events.push("transitioned_to_closed"); + }, 60); + + await handleGCCriticalErrorClose({ + serviceName: "Queue", + getStatus: () => serverStatus, + close: async () => { + closeCalled = true; + }, + logger: createTestLogger(events), + waitIntervalMs: 5, + maxAttempts: 3 + }); + + assert.strictEqual(closeCalled, false, "close should not be called"); + assert.ok( + events.some((e) => e.includes("did not transition to Running state")), + "should log startup transition timeout" + ); + assert.ok( + events.some((e) => + e.includes("Queue server status is Closed (expected Running)") + ), + "should log status mismatch warning" + ); + }); + + it("swallows close errors and logs diagnostics", async () => { + let serverStatus: ServerStatus = ServerStatus.Running; + const events: string[] = []; + + await handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => serverStatus, + close: async () => { + throw new Error("synthetic close failure"); + }, + logger: createTestLogger(events), + waitIntervalMs: 5, + maxAttempts: 1 + }); + + assert.ok( + events.some((e) => + e.includes( + "Blob server error during GC error handling: synthetic close failure" + ) + ), + "should log close diagnostics instead of throwing" + ); + }); +}); diff --git a/tests/blob/upgradeRegression.test.ts b/tests/blob/upgradeRegression.test.ts new file mode 100644 index 000000000..ef44b979d --- /dev/null +++ b/tests/blob/upgradeRegression.test.ts @@ -0,0 +1,660 @@ +import { + newPipeline, + BlobServiceClient, + StorageSharedKeyCredential, + ContainerClient +} from "@azure/storage-blob"; +import * as assert from "assert"; +import * as nodefs from "fs"; +import * as fs from "fs-extra"; + +import BlobConfiguration from "../../src/blob/BlobConfiguration"; +import BlobServer from "../../src/blob/BlobServer"; +import { configLogger } from "../../src/common/Logger"; +import { DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT } from "../../src/blob/utils/constants"; +import { ServerStatus } from "../../src/common/ServerBase"; +import { EMULATOR_ACCOUNT_KEY, EMULATOR_ACCOUNT_NAME } from "../testutils"; + +// Set true to enable debug log +configLogger(false); + +describe("Azurite Upgrade Regression Tests @loki", () => { + let upgradeTestDbPath = ""; + let upgradeTestDbExtentPath = ""; + let upgradeBlobStoragePath = ""; + const cleanupTargets: string[] = []; + const containerName = "upgrade-test-container"; + const blobName = "upgrade-test-blob.txt"; + const blobContent = "This data was created in Azurite 3.35.0"; + + function allocatePaths(scope: string): void { + const token = `${scope}_${Date.now()}_${Math.random() + .toString(16) + .slice(2, 10)}`; + + upgradeTestDbPath = `__test_upgrade_db_blob__${token}.json`; + upgradeTestDbExtentPath = `__test_upgrade_db_blob_extent__${token}.json`; + upgradeBlobStoragePath = `__test_upgrade_blobstorage__${token}`; + + cleanupTargets.push( + upgradeTestDbPath, + upgradeTestDbExtentPath, + upgradeBlobStoragePath + ); + } + + function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async function removePathWithRetry(path: string): Promise { + try { + await nodefs.promises.rm(path, { + recursive: true, + force: true, + maxRetries: 20, + retryDelay: 100 + }); + return; + } catch (err) { + const code = (err as { code?: string })?.code; + + if (code === "ENOENT") { + return; + } + + // Last fallback for sporadic Windows lock races. + await delay(200); + await fs.remove(path); + } + } + + before(async () => { + allocatePaths("suite"); + }); + + beforeEach(() => { + allocatePaths("case"); + }); + + after(async () => { + // Clean up test artifacts + await cleanUpgradeArtifacts(); + }); + + async function cleanUpgradeArtifacts(): Promise { + const paths = Array.from(new Set(cleanupTargets)); + + for (const p of paths) { + try { + await removePathWithRetry(p); + } catch (err) { + const code = (err as { code?: string })?.code; + if (code !== "EPERM") { + throw err; + } + } + } + } + + function extractMd5Bytes(contentMD5: any): number[] { + if (contentMD5 === undefined || contentMD5 === null) { + return []; + } + + if (contentMD5.type === "Buffer" && Array.isArray(contentMD5.data)) { + return contentMD5.data; + } + + if (Array.isArray(contentMD5)) { + return contentMD5; + } + + return Object.keys(contentMD5) + .sort((a, b) => Number(a) - Number(b)) + .map((k) => contentMD5[k]); + } + + function rewritePersistedMd5Shape( + shape: "buffer-json" | "numeric-object" | "plain-array" + ): void { + const db = fs.readJSONSync(upgradeTestDbPath); + const blobCollection = db.collections.find( + (c: any) => c.name === "$BLOBS_COLLECTION$" + ); + + assert.ok(blobCollection !== undefined, "Blob collection should exist"); + + for (const doc of blobCollection.data) { + const existing = doc.properties?.contentMD5; + const bytes = extractMd5Bytes(existing); + assert.ok(bytes.length > 0, "Persisted contentMD5 bytes should exist"); + + if (shape === "buffer-json") { + doc.properties.contentMD5 = { type: "Buffer", data: bytes }; + } else if (shape === "numeric-object") { + const numericObj: { [k: string]: number } = {}; + bytes.forEach((v, i) => { + numericObj[String(i)] = v; + }); + doc.properties.contentMD5 = numericObj; + } else { + doc.properties.contentMD5 = bytes; + } + } + + fs.writeJSONSync(upgradeTestDbPath, db); + } + + function rewritePersistedMd5AsNull(): void { + const db = fs.readJSONSync(upgradeTestDbPath); + const blobCollection = db.collections.find( + (c: any) => c.name === "$BLOBS_COLLECTION$" + ); + + assert.ok(blobCollection !== undefined, "Blob collection should exist"); + + for (const doc of blobCollection.data) { + doc.properties.contentMD5 = null; + } + + fs.writeJSONSync(upgradeTestDbPath, db); + } + + /** + * Simulate the upgrade path + * This test verifies that persisted data can be loaded after version changes + */ + it("should upgrade without data loss", async () => { + // PHASE 1: Simulate old version behavior - create initial data + + const config1 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server1 = new BlobServer(config1); + await server1.start(); + + try { + const baseURL1 = `${server1.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient1 = new BlobServiceClient( + baseURL1, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Create container and blob + const containerClient: ContainerClient = + blobServiceClient1.getContainerClient(containerName); + await containerClient.create(); + + const blockBlobClient = containerClient.getBlockBlobClient(blobName); + await blockBlobClient.upload(blobContent, blobContent.length); + // Verify data was written + const downloadResponse = await blockBlobClient.download(0); + const downloadedStream = downloadResponse.readableStreamBody; + assert.ok( + downloadedStream !== undefined, + "Downloaded stream should exist" + ); + } finally { + await server1.close(); + // Don't clean - we want to preserve the persisted data for the upgrade test + // await server1.clean(); + } + + // Verify persistence files exist + assert.ok( + fs.existsSync(upgradeTestDbPath), + "Blob metadata database should be persisted" + ); + + // PHASE 2: Simulate upgrade - load existing data + + const config2 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server2 = new BlobServer(config2); + + try { + // This is where the bug would occur - loading legacy data with new GC manager + await server2.start(); + assert.ok( + server2.getStatus() === ServerStatus.Running, + "Server should be in Running state" + ); + + const baseURL2 = `${server2.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient2 = new BlobServiceClient( + baseURL2, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Verify existing data is accessible + const containerClient2: ContainerClient = + blobServiceClient2.getContainerClient(containerName); + const blockBlobClient2 = containerClient2.getBlockBlobClient(blobName); + + // Download the blob that was created in phase 1 + const downloadResponse2 = await blockBlobClient2.download(0); + assert.ok( + downloadResponse2.readableStreamBody !== undefined, + "Should be able to download persisted blob after upgrade" + ); + + // PHASE 3: Create new data with new version + const newBlobName = "new-blob.txt"; + const newBlobContent = "This data was created in the new version"; + + const newBlockBlobClient = + containerClient2.getBlockBlobClient(newBlobName); + await newBlockBlobClient.upload(newBlobContent, newBlobContent.length); + + // Verify new data + const newDownloadResponse = await newBlockBlobClient.download(0); + assert.ok( + newDownloadResponse.readableStreamBody !== undefined, + "New blob should be downloadable" + ); + + // Verify old data is still there + const oldDownloadResponse = await blockBlobClient2.download(0); + assert.ok( + oldDownloadResponse.readableStreamBody !== undefined, + "Old blob should still be accessible" + ); + + await server2.close(); + } finally { + try { + await server2.clean(); + } catch (err) { + console.log("Note: Cleanup had an issue, but that's ok for this test"); + } + } + }); + + /** + * Test that verifies the specific scenario from issue #2672: + * Multiple accounts with existing persisted data + */ + it("should handle startup with multiple existing accounts and containers", async () => { + const config3 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server3 = new BlobServer(config3); + await server3.start(); + + try { + const baseURL3 = `${server3.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient3 = new BlobServiceClient( + baseURL3, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Create multiple containers with blobs + const containers = ["container-1", "container-2", "container-3"]; + for (const containerName of containers) { + const containerClient = + blobServiceClient3.getContainerClient(containerName); + await containerClient.create(); + + // Add a blob to each container + for (let i = 0; i < 3; i++) { + const payload = `Content for ${containerName} blob ${i}`; + const blobClient = containerClient.getBlockBlobClient( + `blob-${i}.txt` + ); + await blobClient.upload(payload, Buffer.byteLength(payload)); + } + } + } finally { + await server3.close(); + } + + // Now restart and verify all data is still accessible + const config4 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server4 = new BlobServer(config4); + + try { + await server4.start(); + + const baseURL4 = `${server4.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient4 = new BlobServiceClient( + baseURL4, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Verify all containers and blobs are accessible + const containers = ["container-1", "container-2", "container-3"]; + for (const containerName of containers) { + const containerClient = + blobServiceClient4.getContainerClient(containerName); + + // List blobs to verify they're accessible + let blobCount = 0; + for await (const blob of containerClient.listBlobsFlat()) { + blobCount++; + assert.ok(blob.name !== undefined, "Blob should have a name"); + } + + assert.strictEqual( + blobCount, + 3, + `Container ${containerName} should have 3 blobs` + ); + } + + await server4.close(); + } finally { + try { + await server4.clean(); + } catch (err) { + console.log("Note: Cleanup had an issue, but that's ok for this test"); + } + } + }); + + it("should load persisted metadata across legacy contentMD5 serialization formats", async () => { + const shapes: Array<"buffer-json" | "numeric-object" | "plain-array"> = [ + "buffer-json", + "numeric-object", + "plain-array" + ]; + + for (const shape of shapes) { + allocatePaths(`shape_${shape}`); + const compatibilityContainer = `compat-${shape}`; + const compatibilityBlob = `blob-${shape}.txt`; + const compatibilityContent = `compatibility data for ${shape}`; + + const createConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const createServer = new BlobServer(createConfig); + await createServer.start(); + + try { + const baseURL = `${createServer.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient = new BlobServiceClient( + baseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = blobServiceClient.getContainerClient( + compatibilityContainer + ); + await containerClient.create(); + + const blobClient = + containerClient.getBlockBlobClient(compatibilityBlob); + await blobClient.upload( + compatibilityContent, + compatibilityContent.length + ); + } finally { + await createServer.close(); + } + + rewritePersistedMd5Shape(shape); + + const loadConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const loadServer = new BlobServer(loadConfig); + await loadServer.start(); + + try { + await new Promise((resolve) => setTimeout(resolve, 300)); + assert.strictEqual( + loadServer.getStatus(), + ServerStatus.Running, + `Server should remain Running for md5 shape ${shape}` + ); + + const loadBaseURL = `${loadServer.getHttpServerAddress()}/devstoreaccount1`; + const loadClient = new BlobServiceClient( + loadBaseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = loadClient.getContainerClient( + compatibilityContainer + ); + const blobClient = + containerClient.getBlockBlobClient(compatibilityBlob); + const properties = await blobClient.getProperties(); + assert.ok( + properties.contentMD5 !== undefined, + `contentMD5 should be restored for md5 shape ${shape}` + ); + } finally { + await loadServer.close(); + await loadServer.clean(); + } + } + }); + + it("should handle persisted null contentMD5 without startup failure", async () => { + const compatibilityContainer = "compat-null-md5"; + const compatibilityBlob = "blob-null-md5.txt"; + const compatibilityContent = "compatibility data for null md5"; + + const createConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const createServer = new BlobServer(createConfig); + await createServer.start(); + + try { + const baseURL = `${createServer.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient = new BlobServiceClient( + baseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = blobServiceClient.getContainerClient( + compatibilityContainer + ); + await containerClient.create(); + + const blobClient = containerClient.getBlockBlobClient(compatibilityBlob); + await blobClient.upload( + compatibilityContent, + compatibilityContent.length + ); + } finally { + await createServer.close(); + } + + rewritePersistedMd5AsNull(); + + const loadConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const loadServer = new BlobServer(loadConfig); + await loadServer.start(); + + try { + await new Promise((resolve) => setTimeout(resolve, 300)); + assert.strictEqual( + loadServer.getStatus(), + ServerStatus.Running, + "Server should remain Running for null contentMD5" + ); + + const loadBaseURL = `${loadServer.getHttpServerAddress()}/devstoreaccount1`; + const loadClient = new BlobServiceClient( + loadBaseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = loadClient.getContainerClient( + compatibilityContainer + ); + const blobClient = containerClient.getBlockBlobClient(compatibilityBlob); + const properties = await blobClient.getProperties(); + + assert.strictEqual( + properties.contentMD5, + undefined, + "Null persisted contentMD5 should be restored as undefined" + ); + } finally { + await loadServer.close(); + await loadServer.clean(); + } + }); +}); diff --git a/tests/queue/queueSas.test.ts b/tests/queue/queueSas.test.ts index 4052a99c2..e6caa0917 100644 --- a/tests/queue/queueSas.test.ts +++ b/tests/queue/queueSas.test.ts @@ -242,7 +242,44 @@ describe("Queue SAS test", () => { assert.ok(error); assert.deepEqual(error.statusCode, 403); - assert.deepEqual(error.code, 'AuthenticationFailed'); + assert.deepEqual(error.code, "AuthenticationFailed"); + }); + + it("generateAccountSASQueryParameters should reject duplicate SAS signature query @loki", async () => { + const tmr = new Date(); + tmr.setDate(tmr.getDate() + 1); + + const storageSharedKeyCredential = (serviceClient as any).credential; + + const sas = generateAccountSASQueryParameters( + { + expiresOn: tmr, + permissions: AccountSASPermissions.parse("rwdlacup"), + resourceTypes: AccountSASResourceTypes.parse("sco").toString(), + services: AccountSASServices.parse("btqf").toString(), + protocol: SASProtocol.HttpsAndHttp, + version: "2019-02-02" + }, + storageSharedKeyCredential as StorageSharedKeyCredential + ).toString(); + + const sasWithDuplicateSig = `${sas}&sig=duplicated-signature`; + const sasURL = `${serviceClient.url}?${sasWithDuplicateSig}`; + const serviceClientWithSAS = new QueueServiceClient( + sasURL, + newPipeline(new AnonymousCredential()) + ); + + let error; + try { + await serviceClientWithSAS.getProperties(); + } catch (err) { + error = err; + } + + assert.ok(error); + assert.deepStrictEqual(error.statusCode, 403); + assert.deepStrictEqual(error.code, "AuthenticationFailed"); }); it("Create queue should work with write (w) or create (c) permission in account SAS @loki", async () => { diff --git a/tests/queue/startupErrorRecovery.test.ts b/tests/queue/startupErrorRecovery.test.ts new file mode 100644 index 000000000..f6abd7215 --- /dev/null +++ b/tests/queue/startupErrorRecovery.test.ts @@ -0,0 +1,166 @@ +import * as assert from "assert"; +import * as fs from "fs-extra"; + +import QueueConfiguration from "../../src/queue/QueueConfiguration"; +import QueueServer from "../../src/queue/QueueServer"; +import { configLogger } from "../../src/common/Logger"; +import { DEFAULT_QUEUE_KEEP_ALIVE_TIMEOUT } from "../../src/queue/utils/constants"; +import { ServerStatus } from "../../src/common/ServerBase"; + +// Set true to enable debug log +configLogger(false); + +describe("Queue Server Startup Error Recovery - Issue #2672 @loki", () => { + const testDbPath = "__test_queue_startup_error_db__.json"; + const testDbExtentPath = "__test_queue_startup_error_db_extent__.json"; + const queueStoragePath = "__test_queue_startup_error_storage__"; + + async function startWithTimeout(server: QueueServer): Promise { + let timeout: ReturnType | undefined; + + try { + await Promise.race([ + server.start(), + new Promise((_, reject) => { + timeout = setTimeout(() => { + reject(new Error("Server startup timeout after 10 seconds")); + }, 10000); + + const nodeTimer = timeout as unknown as { unref?: () => void }; + if (typeof nodeTimer.unref === "function") { + nodeTimer.unref(); + } + }) + ]); + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } + } + + after(async () => { + // Clean up test artifacts + [testDbPath, testDbExtentPath, queueStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + }); + + it("Queue: should start successfully when no persisted data exists (fresh start)", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, queueStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + const config = new QueueConfiguration( + "127.0.0.1", + 0, + DEFAULT_QUEUE_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: queueStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new QueueServer(config); + + try { + // Should start without errors + await server.start(); + assert.strictEqual(server.getStatus(), ServerStatus.Running); + + await server.close(); + assert.strictEqual(server.getStatus(), ServerStatus.Closed); + } finally { + await server.clean(); + } + }); + + it("Queue: should handle GC startup errors gracefully without throwing 'Cannot close server in status Starting'", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, queueStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + // Create a corrupted metadata file to simulate legacy data + const corruptedMetadata = { + schemaVersion: "3.35.0", + data: { + collections: [ + { + name: "queues", + data: [ + { + name: "test-queue", + meta: { revision: 0, created: 1600000000000, version: 0 } + } + ] + } + ] + } + }; + + fs.writeFileSync(testDbPath, JSON.stringify(corruptedMetadata, null, 2)); + + const config = new QueueConfiguration( + "127.0.0.1", + 0, + DEFAULT_QUEUE_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: queueStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new QueueServer(config); + + try { + await startWithTimeout(server); + + assert.notStrictEqual( + server.getStatus(), + ServerStatus.Starting, + "Queue server should not remain in Starting state after startup completes" + ); + + if (server.getStatus() === ServerStatus.Running) { + await server.close(); + } + } catch (err) { + if (!(err instanceof Error)) { + throw err; + } + + if (err.message.includes("Cannot close server in status Starting")) { + assert.fail(`Bug not fixed in Queue: ${err.message}`); + } + + // Do not swallow startup failures such as timeout/hang. + throw err; + } finally { + try { + await server.clean(); + } catch { + // Cleanup may fail if startup failed, that's ok + } + } + }); +}); diff --git a/tests/table/auth/sas.test.ts b/tests/table/auth/sas.test.ts index 952e1ae5d..13808c053 100644 --- a/tests/table/auth/sas.test.ts +++ b/tests/table/auth/sas.test.ts @@ -707,4 +707,57 @@ describe("Shared Access Signature (SAS) authentication", () => { stored = await adminClient.getEntity("part1", "row5"); assert.strictEqual(stored.myValue, "value2"); }); + + it("12. duplicate SAS signature query should fail authentication @loki", async () => { + const tableName: string = getUniqueName("sas12"); + + const conn = createConnectionStringForTest(testLocalAzuriteInstance); + const accountName = /AccountName=([^;]*)/.exec(conn)![1]; + const accountKey = /AccountKey=([^;]*)/.exec(conn)![1]; + const baseUrl = getBaseUrlForTest(); + + const adminClient = TableClient.fromConnectionString(conn, tableName, { + allowInsecureConnection: testLocalAzuriteInstance + }); + + await adminClient.createTable(); + + const now = Date.now(); + const expiry = new Date(now + 5 * 60 * 1000).toISOString(); + + const sas = generateTableSasToken({ + accountName, + accountKey, + tableName, + permissions: "a", + expiry + }); + + const duplicateSigToken = `${sas}&sig=duplicated-signature`; + const sasClient = new TableClient( + baseUrl, + tableName, + new AzureSASCredential(duplicateSigToken), + { + allowInsecureConnection: testLocalAzuriteInstance + } + ); + + let error; + try { + await sasClient.createEntity({ + partitionKey: "part1", + rowKey: "row-duplicate-sig", + myValue: "value1" + }); + } catch (err: any) { + error = err; + } + + assert.ok(error); + assert.strictEqual(error.statusCode, 403); + if (error.code) { + assert.strictEqual(error.code, "AuthenticationFailed"); + } + }); }); From f8f16dcc1dba2ab78f6e31f1f39a70a65d4d3f63 Mon Sep 17 00:00:00 2001 From: Akanksha Jain Date: Thu, 30 Jul 2026 18:38:36 +0530 Subject: [PATCH 3/3] Add .npmrc configuration for npm registry --- .npmrc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..2004d4145 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +registry=https://registry.npmjs.org/ +always-auth=true +//registry.npmjs.org/:_authToken=${NPM_TOKEN}