From 3ed03d98e16a57bb93258fedaee55258be96eed8 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 20 Sep 2018 10:28:33 -0700 Subject: [PATCH] feat: enable retry for addItem and add more debug info Signed-off-by: Raymond Feng --- package-lock.json | 483 ++++++++++++++++-- package.json | 10 +- src/controllers/shopping-cart.controller.ts | 9 +- src/repositories/shopping-cart.repository.ts | 36 +- src/utils/retry.ts | 77 +++ .../shopping-cart.controller.acceptance.ts | 39 +- test/unit/utils.retry.unit.ts | 107 ++++ 7 files changed, 673 insertions(+), 88 deletions(-) create mode 100644 src/utils/retry.ts create mode 100644 test/unit/utils.retry.unit.ts diff --git a/package-lock.json b/package-lock.json index 197468945..538b0ffd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,17 @@ "globals": "^11.1.0", "invariant": "^2.2.0", "lodash": "^4.17.5" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, "@babel/types": { @@ -330,19 +341,59 @@ } }, "@loopback/boot": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@loopback/boot/-/boot-0.13.5.tgz", - "integrity": "sha512-E5j788KV91QON+w73CgLbsyjwLh0xoeG6h81GHMTucoBXYrH+jplawNFucX6hytHLgA3KQslXp5ZP1ZZ0FNFmg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@loopback/boot/-/boot-0.14.0.tgz", + "integrity": "sha512-qfHrlzvHA+O4sninJ+XHsUzxSUIMdrgOH++d5LNqMYA3qhlHU8dEt0e8nPdsfKl6FqjD8FXRd5YNKyMCNR7CqQ==", "requires": { - "@loopback/context": "^0.12.8", - "@loopback/core": "^0.11.9", + "@loopback/context": "^0.12.10", + "@loopback/core": "^0.11.11", "@loopback/dist-util": "^0.3.7", - "@loopback/repository": "^0.16.5", - "@loopback/service-proxy": "^0.8.3", + "@loopback/repository": "^0.17.1", + "@loopback/service-proxy": "^0.8.5", "@types/debug": "0.0.30", "@types/glob": "^5.0.35", "debug": "^3.1.0", "glob": "^7.1.2" + }, + "dependencies": { + "@loopback/context": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@loopback/context/-/context-0.12.10.tgz", + "integrity": "sha512-Smc29O5EkNL7fJnmt/gAgHHp1k2HuTIA1cwaCDFc9qKAk7FvwwM+F+BORnvoh6E9c1xIls5+pbowB4zFpepccQ==", + "requires": { + "@loopback/dist-util": "^0.3.7", + "@loopback/metadata": "^0.9.9", + "debug": "^3.1.0", + "uuid": "^3.2.1" + } + }, + "@loopback/core": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@loopback/core/-/core-0.11.11.tgz", + "integrity": "sha512-5wtLtAT/biV5xfWVJabYphzSi0eGiyQtG8mstgdbdHO8NMVAmzeOylYr37denZ+jISFc/1gBzTxjaBtGrzYAOw==", + "requires": { + "@loopback/context": "^0.12.10", + "@loopback/dist-util": "^0.3.7" + } + }, + "@loopback/metadata": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-0.9.9.tgz", + "integrity": "sha512-efT1o4W8sgeApuI6JRx1mVdm/FZsNwoAx6j6+ShEir2qvV1xwL+VAydDrQLdlisJ+0QyICyx0IRyCLV5yBDxzw==", + "requires": { + "debug": "^3.1.0", + "lodash": "^4.17.5", + "reflect-metadata": "^0.1.10" + } + }, + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "@loopback/build": { @@ -379,6 +430,15 @@ "shebang-command": "^1.2.0", "which": "^1.2.9" } + }, + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } } } }, @@ -391,6 +451,16 @@ "@loopback/metadata": "^0.9.8", "debug": "^3.1.0", "uuid": "^3.2.1" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "@loopback/core": { @@ -411,9 +481,9 @@ } }, "@loopback/http-server": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@loopback/http-server/-/http-server-0.3.9.tgz", - "integrity": "sha512-0pjLIsxjz8rCAYBSSLfhC6NLp0OJp0xsbAtdkZ0ljYzDcZTxDgkuTxVErVBVSRhvgJn3UK27wjG6E4OAPy0N9Q==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@loopback/http-server/-/http-server-0.4.0.tgz", + "integrity": "sha512-G/AeYiRgCHiN0EPKcbePn3w0bE45bi5laueUPzN++QtOQMXJuMMNz3yD8uFdIxggRQbvhaF8QGSxERZe9gOXtQ==", "requires": { "@loopback/dist-util": "^0.3.7", "p-event": "^2.0.0" @@ -427,6 +497,16 @@ "debug": "^3.1.0", "lodash": "^4.17.5", "reflect-metadata": "^0.1.10" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "@loopback/openapi-v3": { @@ -440,6 +520,16 @@ "@loopback/repository-json-schema": "^0.10.12", "debug": "^3.1.0", "lodash": "^4.17.5" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "@loopback/openapi-v3-types": { @@ -452,15 +542,55 @@ } }, "@loopback/repository": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-0.16.5.tgz", - "integrity": "sha512-rAKDibpEcHefOZMts13nhtOKbgD9+kZaZpW2H08JFcnpSNM+eEe4KE7T2c/vB1jufUWoP/f2Ij6RhRy+BOtQsw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-0.17.1.tgz", + "integrity": "sha512-fKnM9DbgedC17/IaBAGX9yvhyvhBldBiBkVnzFxQEH+Rw8p3qVV/W0ByVsTezRAQFZou7i6pWP8PNG+d1JNWUQ==", "requires": { - "@loopback/context": "^0.12.8", - "@loopback/core": "^0.11.9", + "@loopback/context": "^0.12.10", + "@loopback/core": "^0.11.11", "@loopback/dist-util": "^0.3.7", "lodash": "^4.17.10", "loopback-datasource-juggler": "^3.23.0" + }, + "dependencies": { + "@loopback/context": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@loopback/context/-/context-0.12.10.tgz", + "integrity": "sha512-Smc29O5EkNL7fJnmt/gAgHHp1k2HuTIA1cwaCDFc9qKAk7FvwwM+F+BORnvoh6E9c1xIls5+pbowB4zFpepccQ==", + "requires": { + "@loopback/dist-util": "^0.3.7", + "@loopback/metadata": "^0.9.9", + "debug": "^3.1.0", + "uuid": "^3.2.1" + } + }, + "@loopback/core": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@loopback/core/-/core-0.11.11.tgz", + "integrity": "sha512-5wtLtAT/biV5xfWVJabYphzSi0eGiyQtG8mstgdbdHO8NMVAmzeOylYr37denZ+jISFc/1gBzTxjaBtGrzYAOw==", + "requires": { + "@loopback/context": "^0.12.10", + "@loopback/dist-util": "^0.3.7" + } + }, + "@loopback/metadata": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-0.9.9.tgz", + "integrity": "sha512-efT1o4W8sgeApuI6JRx1mVdm/FZsNwoAx6j6+ShEir2qvV1xwL+VAydDrQLdlisJ+0QyICyx0IRyCLV5yBDxzw==", + "requires": { + "debug": "^3.1.0", + "lodash": "^4.17.5", + "reflect-metadata": "^0.1.10" + } + }, + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "@loopback/repository-json-schema": { @@ -473,18 +603,32 @@ "@loopback/metadata": "^0.9.8", "@loopback/repository": "^0.16.5", "@types/json-schema": "^6.0.1" + }, + "dependencies": { + "@loopback/repository": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-0.16.5.tgz", + "integrity": "sha512-rAKDibpEcHefOZMts13nhtOKbgD9+kZaZpW2H08JFcnpSNM+eEe4KE7T2c/vB1jufUWoP/f2Ij6RhRy+BOtQsw==", + "requires": { + "@loopback/context": "^0.12.8", + "@loopback/core": "^0.11.9", + "@loopback/dist-util": "^0.3.7", + "lodash": "^4.17.10", + "loopback-datasource-juggler": "^3.23.0" + } + } } }, "@loopback/rest": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-0.22.2.tgz", - "integrity": "sha512-aVq9pumhrYooQcdTz+bjeaUGgNYoOTr8PxkUYsOx7NY639map1ALqnXkizlbHr153zta4lK6jJ9ZtaTVrR9neQ==", - "requires": { - "@loopback/context": "^0.12.8", - "@loopback/core": "^0.11.9", - "@loopback/http-server": "^0.3.9", - "@loopback/openapi-v3": "^0.14.2", - "@loopback/openapi-v3-types": "^0.9.2", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-0.24.0.tgz", + "integrity": "sha512-JAVpfrzG9yg+wEjjAkILDdwGIbvh2Cl4KqRuF6TpnWR7AzuQh5SGHdtayiekJ1EiQ+Klgmp3VvtlMYFh+7hKHw==", + "requires": { + "@loopback/context": "^0.12.10", + "@loopback/core": "^0.11.11", + "@loopback/http-server": "^0.4.0", + "@loopback/openapi-v3": "^0.14.4", + "@loopback/openapi-v3-types": "^0.9.3", "@types/cors": "^2.8.3", "@types/express": "^4.11.1", "@types/http-errors": "^1.6.1", @@ -505,23 +649,137 @@ "qs": "^6.5.2", "strong-error-handler": "^3.2.0", "validator": "^10.4.0" + }, + "dependencies": { + "@loopback/context": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@loopback/context/-/context-0.12.10.tgz", + "integrity": "sha512-Smc29O5EkNL7fJnmt/gAgHHp1k2HuTIA1cwaCDFc9qKAk7FvwwM+F+BORnvoh6E9c1xIls5+pbowB4zFpepccQ==", + "requires": { + "@loopback/dist-util": "^0.3.7", + "@loopback/metadata": "^0.9.9", + "debug": "^3.1.0", + "uuid": "^3.2.1" + } + }, + "@loopback/core": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@loopback/core/-/core-0.11.11.tgz", + "integrity": "sha512-5wtLtAT/biV5xfWVJabYphzSi0eGiyQtG8mstgdbdHO8NMVAmzeOylYr37denZ+jISFc/1gBzTxjaBtGrzYAOw==", + "requires": { + "@loopback/context": "^0.12.10", + "@loopback/dist-util": "^0.3.7" + } + }, + "@loopback/metadata": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-0.9.9.tgz", + "integrity": "sha512-efT1o4W8sgeApuI6JRx1mVdm/FZsNwoAx6j6+ShEir2qvV1xwL+VAydDrQLdlisJ+0QyICyx0IRyCLV5yBDxzw==", + "requires": { + "debug": "^3.1.0", + "lodash": "^4.17.5", + "reflect-metadata": "^0.1.10" + } + }, + "@loopback/openapi-v3": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@loopback/openapi-v3/-/openapi-v3-0.14.4.tgz", + "integrity": "sha512-svobzhLPgRfR3DuNF/ls3941tKlzGJE8LKn76T9I0R/CvKUCf/7wFtXkk0TGwIReqIhZQ1J1l6heB0jbngTU7g==", + "requires": { + "@loopback/context": "^0.12.10", + "@loopback/dist-util": "^0.3.7", + "@loopback/openapi-v3-types": "^0.9.3", + "@loopback/repository-json-schema": "^0.10.14", + "debug": "^3.1.0", + "lodash": "^4.17.5" + } + }, + "@loopback/openapi-v3-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@loopback/openapi-v3-types/-/openapi-v3-types-0.9.3.tgz", + "integrity": "sha512-Ji9Fncsj+uO0gP7tgvmPAhZLV8CFgrLhCMqhsAwuY3BYCnujft7boRjNHFSFAuszr5dN3N8NeRv9krnZ3MFbHQ==", + "requires": { + "@loopback/dist-util": "^0.3.7", + "openapi3-ts": "^1.0.0" + } + }, + "@loopback/repository-json-schema": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/@loopback/repository-json-schema/-/repository-json-schema-0.10.14.tgz", + "integrity": "sha512-uS9tkwKPE9ycrLVmHb7nDFgjEsKFhMZ4pR4H0o+M62FfhlEbXJrABej0QZC/RoS3B46Gpel5Syyq2xU83Xhukg==", + "requires": { + "@loopback/context": "^0.12.10", + "@loopback/dist-util": "^0.3.7", + "@loopback/metadata": "^0.9.9", + "@loopback/repository": "^0.17.1", + "@types/json-schema": "^6.0.1" + } + }, + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "@loopback/service-proxy": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@loopback/service-proxy/-/service-proxy-0.8.3.tgz", - "integrity": "sha512-HrGtTLGODfqznDZWMK/cydKKXzscVdwDxWHqcbVcVjeeZgQJCwuRck1lY0v4umztdu+GEZ14aVhjKsE2OpJKFw==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@loopback/service-proxy/-/service-proxy-0.8.5.tgz", + "integrity": "sha512-fl9yZJPRW52CD1ON0WAFSlCyl9rgk2qx04XzmzD6YIypUYvV+seu5GOfEAuEawc6aPF1bkgQ+iiRETMIH9HVdA==", "requires": { - "@loopback/context": "^0.12.8", - "@loopback/core": "^0.11.9", + "@loopback/context": "^0.12.10", + "@loopback/core": "^0.11.11", "@loopback/dist-util": "^0.3.7", "loopback-datasource-juggler": "^3.23.0" + }, + "dependencies": { + "@loopback/context": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@loopback/context/-/context-0.12.10.tgz", + "integrity": "sha512-Smc29O5EkNL7fJnmt/gAgHHp1k2HuTIA1cwaCDFc9qKAk7FvwwM+F+BORnvoh6E9c1xIls5+pbowB4zFpepccQ==", + "requires": { + "@loopback/dist-util": "^0.3.7", + "@loopback/metadata": "^0.9.9", + "debug": "^3.1.0", + "uuid": "^3.2.1" + } + }, + "@loopback/core": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@loopback/core/-/core-0.11.11.tgz", + "integrity": "sha512-5wtLtAT/biV5xfWVJabYphzSi0eGiyQtG8mstgdbdHO8NMVAmzeOylYr37denZ+jISFc/1gBzTxjaBtGrzYAOw==", + "requires": { + "@loopback/context": "^0.12.10", + "@loopback/dist-util": "^0.3.7" + } + }, + "@loopback/metadata": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-0.9.9.tgz", + "integrity": "sha512-efT1o4W8sgeApuI6JRx1mVdm/FZsNwoAx6j6+ShEir2qvV1xwL+VAydDrQLdlisJ+0QyICyx0IRyCLV5yBDxzw==", + "requires": { + "debug": "^3.1.0", + "lodash": "^4.17.5", + "reflect-metadata": "^0.1.10" + } + }, + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "@loopback/testlab": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@loopback/testlab/-/testlab-0.12.2.tgz", - "integrity": "sha512-UPjTU2N/QfygagJJOUaoC/GaK9nG2m7fwgFtYSp9myyLISCngS+dM2Jm+2S9Hk1GB6LExdvBCeBmA4xyT64SYQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@loopback/testlab/-/testlab-0.13.0.tgz", + "integrity": "sha512-GAEUWqyvaL6qOZTbHzkQS7+KvcgIg1UaPjBI8qBzlTmw5aACFD1+Qb635tt/tdBfLrhZshTpn8cZTP/evUvjyA==", "dev": true, "requires": { "@loopback/dist-util": "^0.3.7", @@ -565,13 +823,22 @@ }, "@sinonjs/formatio": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", "dev": true, "requires": { "samsam": "1.3.0" } }, + "@sinonjs/samsam": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.0.tgz", + "integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==", + "dev": true, + "requires": { + "array-from": "^2.1.1" + } + }, "@types/bcryptjs": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.1.tgz", @@ -819,9 +1086,9 @@ } }, "ajv": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz", - "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz", + "integrity": "sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -884,6 +1151,12 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -1315,7 +1588,7 @@ }, "combined-stream": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "dev": true, "requires": { @@ -1658,9 +1931,9 @@ } }, "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", + "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", "requires": { "ms": "^2.1.1" } @@ -1826,7 +2099,7 @@ }, "es6-promise": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", "dev": true }, @@ -3123,9 +3396,9 @@ "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=" }, "lolex": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.4.tgz", - "integrity": "sha512-Gh6Vffq/piTeHwunLNFR1jFVaqlwK9GMNUxFcsO1cwHyvbRKHwX8UDkxmrDnbcPdHNmpv7z2kxtkkSx5xkNpMw==", + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", "dev": true }, "longest": { @@ -3145,6 +3418,16 @@ "msgpack5": "^4.2.0", "strong-globalize": "^4.1.1", "uuid": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "loopback-connector-kv-redis": { @@ -3156,6 +3439,16 @@ "ioredis": "^3.2.2", "loopback-connector": "^4.0.0", "strong-globalize": "^4.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "loopback-connector-mongodb": { @@ -3169,12 +3462,22 @@ "loopback-connector": "^4.5.0", "mongodb": "^3.1.4", "strong-globalize": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "loopback-datasource-juggler": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-3.23.0.tgz", - "integrity": "sha512-bVFlSSThPBdfTwsWRSW7xtk3UKKdAHY4pUnBnOaPAvshZc6gxAJEiiErOvCHaOEP4WDg8p5o13wdIN1qi4b9VQ==", + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-3.24.0.tgz", + "integrity": "sha512-A4ehlQUGTp0g8qB2P9nAPLy23sZRdWboHax335V5gONevwaoM+O01L03aud1GJL71AvkHVs0yar4E8IWzbPCKw==", "requires": { "async": "^2.6.0", "bluebird": "^3.1.1", @@ -3189,6 +3492,16 @@ "strong-globalize": "^4.1.1", "traverse": "^0.6.6", "uuid": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "loose-envify": { @@ -3520,18 +3833,27 @@ "dev": true }, "nise": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.4.tgz", - "integrity": "sha512-pxE0c9PzgrUTyhfv5p+5eMIdfU2bLEsq8VQEuE0kxM4zP7SujSar7rk9wpI2F7RyyCEvLyj5O7Is3RER5F36Fg==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.5.tgz", + "integrity": "sha512-OHRVvdxKgwZELf2DTgsJEIA4MOq8XWvpSUzoOXyxJ2mY0mMENWC66+70AShLR2z05B1dzrzWlUQJmJERlOUpZw==", "dev": true, "requires": { - "@sinonjs/formatio": "^2.0.0", + "@sinonjs/formatio": "3.0.0", "just-extend": "^3.0.0", "lolex": "^2.3.2", "path-to-regexp": "^1.7.0", "text-encoding": "^0.6.4" }, "dependencies": { + "@sinonjs/formatio": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.0.0.tgz", + "integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==", + "dev": true, + "requires": { + "@sinonjs/samsam": "2.1.0" + } + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -6602,7 +6924,7 @@ }, "sinon": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", + "resolved": "http://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", "dev": true, "requires": { @@ -6805,6 +7127,17 @@ "strong-task-emitter": "^0.0.8", "typedoc": "^0.12.0", "underscore.string": "^3.3.4" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, "strong-error-handler": { @@ -6819,6 +7152,16 @@ "http-status": "^1.1.2", "js2xmlparser": "^3.0.0", "strong-globalize": "^4.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "strong-globalize": { @@ -6834,6 +7177,16 @@ "mkdirp": "^0.5.1", "os-locale": "^2.0.0", "yamljs": "^0.3.0" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "requires": { + "ms": "^2.1.1" + } + } } }, "strong-task-emitter": { @@ -6843,6 +7196,17 @@ "dev": true, "requires": { "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, "superagent": { @@ -6861,6 +7225,17 @@ "mime": "^1.4.1", "qs": "^6.5.1", "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, "supertest": { @@ -6884,7 +7259,7 @@ }, "swagger2openapi": { "version": "2.11.16", - "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-2.11.16.tgz", + "resolved": "http://registry.npmjs.org/swagger2openapi/-/swagger2openapi-2.11.16.tgz", "integrity": "sha512-5Pv20whg9Bn1dqKDuGatH0r+7mAEU8+tHJU1PdT9ufGeZNseqJBs9Y8AOd8EMEHKRuAgAzYYWhINvgtodAlZRA==", "dev": true, "requires": { @@ -7231,7 +7606,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { diff --git a/package.json b/package.json index 965f1241b..8ec23730c 100644 --- a/package.json +++ b/package.json @@ -52,14 +52,15 @@ "src" ], "dependencies": { - "@loopback/boot": "^0.13.5", + "@loopback/boot": "^0.14.0", "@loopback/context": "^0.12.8", "@loopback/core": "^0.11.9", "@loopback/dist-util": "^0.3.7", "@loopback/openapi-v3": "^0.14.2", - "@loopback/repository": "^0.16.5", - "@loopback/rest": "^0.22.2", + "@loopback/repository": "^0.17.1", + "@loopback/rest": "^0.24.0", "bcryptjs": "^2.4.3", + "debug": "^4.0.1", "isemail": "^3.1.3", "loopback-connector-kv-redis": "^3.0.0", "loopback-connector-mongodb": "^3.7.1" @@ -69,8 +70,9 @@ "@commitlint/config-conventional": "^7.1.2", "@commitlint/travis-cli": "^7.1.2", "@loopback/build": "^0.7.3", - "@loopback/testlab": "^0.12.2", + "@loopback/testlab": "^0.13.0", "@types/bcryptjs": "^2.4.1", + "@types/debug": "0.0.30", "@types/mocha": "^5.0.0", "@types/node": "^10.9.4", "commitizen": "^2.10.1", diff --git a/src/controllers/shopping-cart.controller.ts b/src/controllers/shopping-cart.controller.ts index 36063e085..36b9fc1ec 100644 --- a/src/controllers/shopping-cart.controller.ts +++ b/src/controllers/shopping-cart.controller.ts @@ -15,6 +15,8 @@ import { import {repository} from '@loopback/repository'; import {ShoppingCartRepository} from '../repositories'; import {ShoppingCart, ShoppingCartItem} from '../models'; +import * as debugFactory from 'debug'; +const debug = debugFactory('loopback:example:shopping'); /** * Controller for shopping cart @@ -35,6 +37,7 @@ export class ShoppingCartController { @param.path.string('userId') userId: string, @requestBody({description: 'shopping cart'}) cart: ShoppingCart, ) { + debug('Create shopping cart %s: %j', userId, cart); if (userId !== cart.userId) { throw new HttpErrors.BadRequest( `User id does not match: ${userId} !== ${cart.userId}`, @@ -49,7 +52,9 @@ export class ShoppingCartController { */ @get('/shoppingCarts/{userId}') async get(@param.path.string('userId') userId: string) { + debug('Get shopping cart %s', userId); const cart = await this.shoppingCartRepository.get(userId); + debug('Shopping cart %s: %j', userId, cart); if (cart == null) { throw new HttpErrors.NotFound( `Shopping cart not found for user: ${userId}`, @@ -65,6 +70,7 @@ export class ShoppingCartController { */ @del('/shoppingCarts/{userId}') async remove(@param.path.string('userId') userId: string) { + debug('Remove shopping cart %s', userId); await this.shoppingCartRepository.delete(userId); } @@ -78,6 +84,7 @@ export class ShoppingCartController { @param.path.string('userId') userId: string, @requestBody({description: 'shopping cart item'}) item: ShoppingCartItem, ) { - await this.shoppingCartRepository.addItem(userId, item); + debug('Add item %j to shopping cart %s', item, userId); + return this.shoppingCartRepository.addItem(userId, item); } } diff --git a/src/repositories/shopping-cart.repository.ts b/src/repositories/shopping-cart.repository.ts index b3f3bd777..16e37c04c 100644 --- a/src/repositories/shopping-cart.repository.ts +++ b/src/repositories/shopping-cart.repository.ts @@ -8,6 +8,7 @@ import {ShoppingCart, ShoppingCartItem} from '../models/shopping-cart.model'; import {RedisDataSource} from '../datasources/redis.datasource'; import {inject} from '@loopback/context'; import {promisify} from 'util'; +import {Task, retry} from '../utils/retry'; export class ShoppingCartRepository extends DefaultKeyValueRepository< ShoppingCart @@ -18,19 +19,32 @@ export class ShoppingCartRepository extends DefaultKeyValueRepository< /** * Add an item to the shopping cart with optimistic lock to allow concurrent - * `adding to cart` from multiple devices + * `adding to cart` from multiple devices. If race condition happens, it will + * try 10 times at an interval of 10 ms. Timeout will be reported as an error. * * @param userId User id * @param item Item to be added + * @returns A promise that's resolved with the updated ShoppingCart instance + * */ addItem(userId: string, item: ShoppingCartItem) { - const addItemToCart = (cart: ShoppingCart | null) => { - cart = cart || new ShoppingCart({userId}); - cart.items = cart.items || []; - cart.items.push(item); - return cart; + const task: Task = { + run: async () => { + const addItemToCart = (cart: ShoppingCart | null) => { + cart = cart || new ShoppingCart({userId}); + cart.items = cart.items || []; + cart.items.push(item); + return cart; + }; + const result = await this.checkAndSet(userId, addItemToCart); + return { + done: result != null, + value: result, + }; + }, + description: `update the shopping cart for '${userId}'`, }; - return this.checkAndSet(userId, addItemToCart); + return retry(task, {maxTries: 10, interval: 10}); } /** @@ -42,6 +56,10 @@ export class ShoppingCartRepository extends DefaultKeyValueRepository< * @param userId User id * @param check A function that checks the current value and produces a new * value. It returns `null` to abort. + * + * @returns A promise that's resolved with the updated ShoppingCart instance + * or with null if the transaction failed due to a race condition. + * See https://github.com/NodeRedis/node_redis#optimistic-locks */ async checkAndSet( userId: string, @@ -66,7 +84,7 @@ export class ShoppingCartRepository extends DefaultKeyValueRepository< if (!cart) return null; await execute('MULTI', []); await this.set(userId, cart); - await execute('EXEC', []); - return cart; + const result = await execute('EXEC', []); + return result == null ? null : cart; } } diff --git a/src/utils/retry.ts b/src/utils/retry.ts new file mode 100644 index 000000000..98c0a5180 --- /dev/null +++ b/src/utils/retry.ts @@ -0,0 +1,77 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {HttpErrors} from '@loopback/rest'; +import {promisify} from 'util'; +import * as debugFactory from 'debug'; +const debug = debugFactory('loopback:example:shopping'); + +export interface TaskStatus { + done: boolean; + value?: T | null; +} + +/** + * A task that can be retried + */ +export interface Task { + run(): Promise>; + description: string; +} + +/** + * Options for retry + */ +export interface RetryOptions { + /** + * Maximum number of tries including the first run. + */ + maxTries?: number; + /** + * Milliseconds to wait after each try + */ + interval?: number; +} + +/** + * Retry a task for number of times with the given interval in ms + * @param task Task object {run, description} + * @param maxTries Maximum number of tries (including the first run), + * default to 10 + * @param interval Milliseconds to wait after each try, default to 100ms + */ +export async function retry( + task: Task, + {maxTries = 10, interval = 100}: RetryOptions = {}, +): Promise { + if (maxTries < 1) maxTries = 1; + let triesLeft = maxTries; + while (true) { + debug( + 'Try %s (%d/%d)', + task.description, + maxTries - triesLeft + 1, + maxTries, + ); + const status = await task.run(); + if (status.done) return status.value!; + if (--triesLeft > 0) { + debug('Wait for %d ms', interval); + await sleep(interval); + } else { + // No more retries, timeout + const msg = `Failed to ${task.description} after ${maxTries * + interval} ms`; + debug('%s', msg); + throw new HttpErrors.RequestTimeout(msg); + } + } +} + +/** + * Sleep for the given milliseconds + * @param ms Number of milliseconds to wait + */ +export const sleep = promisify(setTimeout); // (ms: number) => Promise diff --git a/test/acceptance/shopping-cart.controller.acceptance.ts b/test/acceptance/shopping-cart.controller.acceptance.ts index dc84adb0a..7dcfcd374 100644 --- a/test/acceptance/shopping-cart.controller.acceptance.ts +++ b/test/acceptance/shopping-cart.controller.acceptance.ts @@ -41,7 +41,7 @@ describe('ShoppingCartController', () => { .put(`/shoppingCarts/${cart.userId}`) .set('Content-Type', 'application/json') .send(cart) - .expect(200); + .expect(204); }); it('throws error if userId does not match the cart', async () => { @@ -59,7 +59,7 @@ describe('ShoppingCartController', () => { await client .put(`/shoppingCarts/${cart.userId}`) .send(cart) - .expect(200); + .expect(204); await client .get(`/shoppingCarts/${cart.userId}`) .expect(200, cart.toJSON()); @@ -71,13 +71,13 @@ describe('ShoppingCartController', () => { await client .put(`/shoppingCarts/${cart.userId}`) .send(cart) - .expect(200); + .expect(204); // Now we can see it await client .get(`/shoppingCarts/${cart.userId}`) .expect(200, cart.toJSON()); // Delete the shopping cart - await client.del(`/shoppingCarts/${cart.userId}`).expect(200); + await client.del(`/shoppingCarts/${cart.userId}`).expect(204); // Now it's gone await client.get(`/shoppingCarts/${cart.userId}`).expect(404); }); @@ -89,7 +89,7 @@ describe('ShoppingCartController', () => { await client .put(`/shoppingCarts/${cart.userId}`) .send(cart) - .expect(200); + .expect(204); // Now we can see it await client .post(`/shoppingCarts/${cart.userId}/items`) @@ -117,24 +117,23 @@ describe('ShoppingCartController', () => { await cartRepo.deleteAll(); } + function givenAnItem(item?: Partial) { + return new ShoppingCartItem( + Object.assign( + { + productId: 'iPhone XS', + quantity: 2, + price: 2000, + }, + item, + ), + ); + } + function givenShoppingCart() { return new ShoppingCart({ userId: 'user-0001', - items: [ - new ShoppingCartItem({ - productId: 'iPhone XS Max', - quantity: 1, - price: 1200, - }), - ], - }); - } - - function givenAnItem() { - return new ShoppingCartItem({ - productId: 'iPhone XS', - quantity: 2, - price: 2000, + items: [givenAnItem()], }); } }); diff --git a/test/unit/utils.retry.unit.ts b/test/unit/utils.retry.unit.ts new file mode 100644 index 000000000..aaef5d214 --- /dev/null +++ b/test/unit/utils.retry.unit.ts @@ -0,0 +1,107 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {sleep, Task, retry} from '../../src/utils/retry'; + +describe('sleep()', () => { + it('waits for given milliseconds', async () => { + const start = Date.now(); + await sleep(10); + const duration = Date.now() - start; + expect(duration).to.be.greaterThanOrEqual(10); + }); + + it('defaults invalid time to 1 ms', async () => { + const start = Date.now(); + await sleep(-100); + const duration = Date.now() - start; + expect(duration).to.be.greaterThanOrEqual(0); + }); +}); + +describe('retry()', () => { + it('runs once if done', async () => { + let count = 0; + const fn = () => { + // Always good on the 1st run + count++; + return 100; + }; + const task = givenTask(fn); + const num = await retry(task, {maxTries: 5, interval: 1}); + expect(num).to.eql(100); + expect(count).to.eql(1); + }); + + it('retries until done', async () => { + let count = 0; + const fn = () => { + if (count++ < 2) return null; + return 1; + }; + const task = givenTask(fn); + const num = await retry(task, {maxTries: 5, interval: 1}); + expect(num).to.eql(1); + expect(count).to.eql(3); + }); + + it('retries until done if error is ignored', async () => { + let count = 0; + const fn = () => { + if (count++ < 1) return null; + if (count === 2) throw new Error('fail'); + return 1; + }; + const task = givenTask(fn, true); + const num = await retry(task, {maxTries: 5, interval: 1}); + expect(num).to.eql(1); + expect(count).to.eql(3); + }); + + it('retries fails if error is thrown', async () => { + let count = 0; + const fn = () => { + if (count++ < 1) return null; + if (count === 2) throw new Error('fail'); + return 1; + }; + const task = givenTask(fn); + return await expect( + retry(task, {maxTries: 5, interval: 1}), + ).to.be.rejectedWith(/fail/); + }); + + it('retries fails with timeout', async () => { + let count = 0; + const fn = () => { + if (count++ < 10) return null; + return count; + }; + const task = givenTask(fn); + return await expect( + retry(task, {maxTries: 5, interval: 1}), + ).to.be.rejectedWith(/Failed to count after 5 ms/); + }); +}); + +function givenTask(fn: () => T | null | undefined, ignoreError = false) { + const task: Task = { + run: async () => { + try { + const result = fn(); + return {done: result != null, value: result}; + } catch (err) { + if (ignoreError) { + return {done: false}; + } else { + throw err; + } + } + }, + description: 'count', + }; + return task; +}