diff --git a/CHANGELOG.md b/CHANGELOG.md index 170bcaf..780866f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 3.3.1 - 2022-10-22 + +**Fixed** + +- Fixes [#73](https://github.com/stackkit/laravel-google-cloud-tasks-queue/issues/73) Cannot access protected property Illuminate\Queue\Jobs\SyncJob::$job + +## 3.3.0 - 2022-10-15 + +**Added** + +- Jobs can now be released back onto the queue. + +## 3.2.1 - 2022-09-02 + +**Fixed** + +- Jobs were dispatched before a DB commit if `after_commit` or `afterCommit()` was used. This has now been corrected. + +**Added** + +- Request validation for the TaskHandler endpoint. + +## 3.2.0 - 2022-08-13 + +**Added** + +- Added support for jobs that use the `Illuminate\Contracts\Queue\ShouldBeEncrypted` contract + +## 3.1.4 - 2022-06-24 + +**Fixed** + +- Fixed usage of incorrect header to set count retries ([#55](https://github.com/stackkit/laravel-google-cloud-tasks-queue/discussions/55)) +- Fix getRetryUntilTimestamp not working due to incomplete task name ([#56](https://github.com/stackkit/laravel-google-cloud-tasks-queue/discussions/56)) + ## 3.1.3 - 2022-06-19 **Fixed** diff --git a/README.md b/README.md index 5fa0a04..bb824a3 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Please check the table below for supported Laravel and PHP versions: // does not respond by this deadline then the request is cancelled and the attempt // is marked as a DEADLINE_EXCEEDED failure. 'dispatch_deadline' => null, + 'backoff' => 0, ], ``` @@ -79,13 +80,19 @@ Please check the table below on what the values mean and what their value should
- How it works + How it works & Differences
Using Cloud Tasks as a Laravel queue driver is fundamentally different than other Laravel queue drivers, like Redis. Typically a Laravel queue has a worker that listens to incoming jobs using the `queue:work` / `queue:listen` command. With Cloud Tasks, this is not the case. Instead, Cloud Tasks will schedule the job for you and make an HTTP request to your application with the job payload. There is no need to run a `queue:work/listen` command. + +#### Good to know + +- The "Min backoff" and "Max backoff" options in Cloud Tasks are ignored. This is intentional: Laravel has its own backoff feature (which is more powerful than what Cloud Tasks offers) and therefore I have chosen that over the Cloud Tasks one. +- Similarly to the backoff feature, I have also chosen to let the package do job retries the 'Laravel way'. In Cloud Tasks, when a task throws an exception, Cloud Tasks will decide for itself when to retry the task (based on the backoff values). It will also manage its own state and knows how many times a task has been retried. This is different from Laravel. In typical Laravel queues, when a job throws an exception, the job is deleted and released back onto the queue. In order to support Laravel's backoff feature, this package must behave the same way about job retries. +
Dashboard (beta) diff --git a/composer.json b/composer.json index c01c313..76ee4cc 100644 --- a/composer.json +++ b/composer.json @@ -36,5 +36,23 @@ "Stackkit\\LaravelGoogleCloudTasksQueue\\CloudTasksServiceProvider" ] } + }, + "scripts": { + "l9": [ + "composer require laravel/framework:9.* orchestra/testbench:7.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ], + "l8": [ + "composer require laravel/framework:8.* orchestra/testbench:6.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ], + "l7": [ + "composer require laravel/framework:7.* orchestra/testbench:5.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ], + "l6": [ + "composer require laravel/framework:6.* orchestra/testbench:4.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ] } } diff --git a/dashboard/dist/assets/index.1002db9a.css b/dashboard/dist/assets/index.d8eef428.css similarity index 90% rename from dashboard/dist/assets/index.1002db9a.css rename to dashboard/dist/assets/index.d8eef428.css index 9c56a8b..6721fae 100644 --- a/dashboard/dist/assets/index.1002db9a.css +++ b/dashboard/dist/assets/index.d8eef428.css @@ -1 +1 @@ -.router-link-active{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity));font-weight:700;--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}:root{--popper-theme-background-color: #333333;--popper-theme-background-color-hover: #333333;--popper-theme-text-color: #ffffff;--popper-theme-border-width: 0px;--popper-theme-border-style: solid;--popper-theme-border-radius: 6px;--popper-theme-padding: 4px;--popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, .25)}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.right-0{right:0}.top-0{top:0}.-left-1{left:-.25rem}.mb-4{margin-bottom:1rem}.mt-8{margin-top:2rem}.mb-2{margin-bottom:.5rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.mt-3{margin-top:.75rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.-ml-1{margin-left:-.25rem}.mr-3{margin-right:.75rem}.ml-4{margin-left:1rem}.mt-12{margin-top:3rem}.ml-10{margin-left:2.5rem}.mb-6{margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.h-2{height:.5rem}.h-full{height:100%}.h-4{height:1rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-\[250px\]{width:250px}.w-\[300px\]{width:300px}.w-full{width:100%}.w-2{width:.5rem}.w-\[50px\]{width:50px}.w-\[100px\]{width:100px}.w-\[150px\]{width:150px}.w-\[200px\]{width:200px}.w-4{width:1rem}.w-5{width:1.25rem}.w-2\/12{width:16.666667%}.max-w-\[calc\(100\%-250px\)\]{max-width:calc(100% - 250px)}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.shrink-0{flex-shrink:0}.basis-auto{flex-basis:auto}.basis-\[400px\]{flex-basis:400px}.basis-\[250px\]{flex-basis:250px}.table-fixed{table-layout:fixed}.translate-x-\[300px\]{--tw-translate-x: 300px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-blue-300\/30{background-color:#93c5fd4d}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-red-100\/50{background-color:#fee2e280}.bg-white\/90{background-color:#ffffffe6}.p-6{padding:1.5rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0{padding-top:0;padding-bottom:0}.px-4{padding-left:1rem;padding-right:1rem}.pr-6{padding-right:1.5rem}.pt-1{padding-top:.25rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-medium{font-weight:500}.font-light{font-weight:300}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.tracking-wider{letter-spacing:.05em}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-indigo-100{--tw-text-opacity: 1;color:rgb(224 231 255 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.text-red-600\/50{color:#dc262680}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-black\/70{color:#000000b3}.text-black\/20{color:#0003}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-white{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:scale-\[1\.1\]:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-indigo-100\/10:hover{background-color:#e0e7ff1a}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}@media (prefers-color-scheme: dark){.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.dark\:text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}}@media (min-width: 640px){.sm\:rounded-lg{border-radius:.5rem}}@-webkit-keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}@keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}.shake[data-v-59cd799c]{-webkit-animation:shake-59cd799c .3s linear;animation:shake-59cd799c .3s linear}input[type=password][data-v-59cd799c]{font:small-caption;font-size:36px}.task-error{background-color:#fee2e280;color:#dc262680}.task-queued,.task-scheduled{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-successful{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.task-queued{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.task-error[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.task-queued[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.task-running[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.no-scroll[data-v-d792a216]::-webkit-scrollbar{display:none} +.router-link-active{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity));font-weight:700;--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}:root{--popper-theme-background-color: #333333;--popper-theme-background-color-hover: #333333;--popper-theme-text-color: #ffffff;--popper-theme-border-width: 0px;--popper-theme-border-style: solid;--popper-theme-border-radius: 6px;--popper-theme-padding: 4px;--popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, .25)}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.right-0{right:0}.top-0{top:0}.-left-1{left:-.25rem}.mb-4{margin-bottom:1rem}.mt-8{margin-top:2rem}.mb-2{margin-bottom:.5rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.mt-3{margin-top:.75rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.-ml-1{margin-left:-.25rem}.mr-3{margin-right:.75rem}.ml-4{margin-left:1rem}.mt-12{margin-top:3rem}.ml-10{margin-left:2.5rem}.mb-6{margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.h-2{height:.5rem}.h-full{height:100%}.h-4{height:1rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-\[250px\]{width:250px}.w-\[300px\]{width:300px}.w-full{width:100%}.w-2{width:.5rem}.w-\[50px\]{width:50px}.w-\[100px\]{width:100px}.w-\[150px\]{width:150px}.w-\[200px\]{width:200px}.w-4{width:1rem}.w-5{width:1.25rem}.w-2\/12{width:16.666667%}.max-w-\[calc\(100\%-250px\)\]{max-width:calc(100% - 250px)}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.shrink-0{flex-shrink:0}.basis-auto{flex-basis:auto}.basis-\[400px\]{flex-basis:400px}.basis-\[250px\]{flex-basis:250px}.table-fixed{table-layout:fixed}.translate-x-\[300px\]{--tw-translate-x: 300px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-blue-300\/30{background-color:#93c5fd4d}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-red-100\/50{background-color:#fee2e280}.bg-white\/90{background-color:#ffffffe6}.p-6{padding:1.5rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0{padding-top:0;padding-bottom:0}.px-4{padding-left:1rem;padding-right:1rem}.pr-6{padding-right:1.5rem}.pt-1{padding-top:.25rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-medium{font-weight:500}.font-light{font-weight:300}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.tracking-wider{letter-spacing:.05em}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-indigo-100{--tw-text-opacity: 1;color:rgb(224 231 255 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.text-red-600\/50{color:#dc262680}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-black\/70{color:#000000b3}.text-black\/20{color:#0003}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-white{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:scale-\[1\.1\]:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-indigo-100\/10:hover{background-color:#e0e7ff1a}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}@media (prefers-color-scheme: dark){.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.dark\:text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}}@media (min-width: 640px){.sm\:rounded-lg{border-radius:.5rem}}@-webkit-keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}@keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}.shake[data-v-59cd799c]{-webkit-animation:shake-59cd799c .3s linear;animation:shake-59cd799c .3s linear}input[type=password][data-v-59cd799c]{font:small-caption;font-size:36px}.task-error{background-color:#fee2e280;color:#dc262680}.task-queued,.task-scheduled{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running,.task-released{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.task-queued{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.task-error[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.task-queued[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.task-running[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.no-scroll[data-v-5df67d4c]::-webkit-scrollbar{display:none} diff --git a/dashboard/dist/assets/index.5a46c6a0.js b/dashboard/dist/assets/index.ea68d73f.js similarity index 52% rename from dashboard/dist/assets/index.5a46c6a0.js rename to dashboard/dist/assets/index.ea68d73f.js index 5a57c92..e61a7bc 100644 --- a/dashboard/dist/assets/index.5a46c6a0.js +++ b/dashboard/dist/assets/index.ea68d73f.js @@ -1 +1 @@ -var W=Object.defineProperty,J=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var D=Object.getOwnPropertySymbols;var Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var E=(s,a,t)=>a in s?W(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t,C=(s,a)=>{for(var t in a||(a={}))Y.call(a,t)&&E(s,t,a[t]);if(D)for(var t of D(a))Z.call(a,t)&&E(s,t,a[t]);return s},V=(s,a)=>J(s,X(a));var b=(s,a,t)=>new Promise((r,o)=>{var l=c=>{try{i(t.next(c))}catch(u){o(u)}},n=c=>{try{i(t.throw(c))}catch(u){o(u)}},i=c=>c.done?r(c.value):Promise.resolve(c.value).then(l,n);i((t=t.apply(s,a)).next())});import{r as S,o as d,c as _,a as h,w as x,n as g,F as w,b as y,d as e,e as ee,f as te,u as k,g as m,h as A,i as O,v as se,j as B,p as F,k as N,t as p,l as M,m as oe,q as ae,s as ne,x as q,y as $,z as j,A as le,B as re,C as ie,D as ce}from"./vendor.433de25e.js";const ue=function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const n of l.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&r(n)}).observe(document,{childList:!0,subtree:!0});function t(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerpolicy&&(l.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?l.credentials="include":o.crossorigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(o){if(o.ep)return;o.ep=!0;const l=t(o);fetch(o.href,l)}};ue();var T=(s,a)=>{const t=s.__vccOpts||s;for(const[r,o]of a)t[r]=o;return t};const de={},pe=y("Dashboard "),_e=y("Recent "),he=y("Queued "),me=y("Failed ");function fe(s,a){var r,o,l,n,i,c,u,f,v;const t=S("router-link");return d(),_(w,null,[h(t,{to:{name:"home"},class:"block p-4 rounded mb-2 cursor-pointer"},{default:x(()=>[pe]),_:1}),h(t,{to:{name:"recent"},class:g(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((l=(o=(r=s.$route)==null?void 0:r.matched[0])==null?void 0:o.meta)==null?void 0:l.route)==="recent"}])},{default:x(()=>[_e]),_:1},8,["class"]),h(t,{to:{name:"queued"},class:g(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((c=(i=(n=s.$route)==null?void 0:n.matched[0])==null?void 0:i.meta)==null?void 0:c.route)==="queued"}])},{default:x(()=>[he]),_:1},8,["class"]),h(t,{to:{name:"failed"},class:g(["block p-4 rounded mb-2",{"router-link-active":((v=(f=(u=s.$route)==null?void 0:u.matched[0])==null?void 0:f.meta)==null?void 0:v.route)==="failed"}])},{default:x(()=>[me]),_:1},8,["class"])],64)}var xe=T(de,[["render",fe]]);const ve={class:"flex"},ge={class:"basis-auto w-[250px] shrink-0 bg-white p-6 min-h-screen"},ye={class:"flex-1 max-w-[calc(100%-250px)] p-6"},be={setup(s){return(a,t)=>{const r=S("router-view");return d(),_("div",ve,[e("aside",ge,[h(xe)]),e("div",ye,[h(r)])])}}};function I(){return b(this,arguments,function*({endpoint:s,router:a,body:t=null,method:r="GET",login:o=!1}={}){const l=yield fetch(`/cloud-tasks-api/${s}`,V(C({method:r},t?{body:t}:{}),{headers:C({},o?{}:{Authorization:`Bearer ${localStorage.getItem("cloud-tasks-token")}`})}));return l.status===403&&!o&&(localStorage.removeItem("cloud-tasks-token"),a.push({name:"login"})),o?yield l.text():yield l.json()})}function L(r){return b(this,arguments,function*(s,a={},t){let o=!1;const l=function(c){return b(this,null,function*(){if(o)return;const u=new URL(window.location.href),f=new URLSearchParams(u.search);for(const[v,G]of Object.entries(a))f.append(v,G);o=!0,c.value=yield I({endpoint:`tasks?${f.toString()}`,router:t}),o=!1})};l(s);let n=setInterval(()=>l(s),3e3);ee(function(){setTimeout(()=>l(s))});const i=function(){document.visibilityState==="visible"?(l(s),clearInterval(n),n=setInterval(()=>l(s),3e3)):document.visibilityState==="hidden"&&clearInterval(n)};document.addEventListener("visibilitychange",i),te(()=>{clearInterval(n),document.removeEventListener("visibilitychange",i),o=!1})})}const z=s=>(F("data-v-59cd799c"),s=s(),N(),s),we={class:"block w-full h-full flex items-center justify-center"},ke=z(()=>e("h3",{class:"text-4xl"},"This application is password protected.",-1)),$e=["onKeyup","disabled"],Ce=z(()=>e("div",{class:"text-center mt-6 text-xl"},[y(" Press "),e("span",{class:"bg-blue-200 py-1 px-2 ml-2 mr-2 rounded text-blue-800"},"Enter"),y(" to log in. ")],-1)),qe={setup(s){const a=k(),t=m(null),r=m(""),o=m(""),l=m(!1),n=m(!1);A(()=>{t.value.focus()});function i(){return b(this,null,function*(){if(r.value===""||r.value===o.value)return;o.value=r.value;const c=new FormData;c.append("password",r.value),n.value=!0;const u=yield I({endpoint:"login",method:"POST",body:c,login:!0});n.value=!1,u?(localStorage.setItem("cloud-tasks-token",u),a.push({name:"home"})):(l.value=!0,setTimeout(()=>{l.value=!1},820),setTimeout(()=>t.value.focus(),50))})}return(c,u)=>(d(),_("div",we,[e("div",null,[ke,O(e("input",{type:"password",class:g(["w-full p-2 px-6 text-2xl font-light mt-8 text-center outline-none shadow rounded-full",{shake:l.value}]),onKeyup:B(i,["enter"]),"onUpdate:modelValue":u[0]||(u[0]=f=>r.value=f),disabled:n.value,ref_key:"inputRef",ref:t},null,42,$e),[[se,r.value]]),Ce])]))}};var Te=T(qe,[["__scopeId","data-v-59cd799c"]]);const Se=e("h3",{class:"text-3xl mb-4"},"All tasks",-1),Ae={class:"grid grid-cols-3 gap-4"},Ie=["textContent"],Le=e("span",{class:"text-gray-600"},"this minute",-1),Re=["textContent"],Pe=e("span",{class:"text-gray-600"},"this hour",-1),Ue=["textContent"],De=e("span",{class:"text-gray-600"},"today",-1),Ee=e("h3",{class:"text-3xl mb-4 mt-8"},"Failed tasks",-1),Ve={class:"grid grid-cols-3 gap-4"},Oe=["textContent"],Be=e("span",{class:"text-gray-600"},"this minute",-1),Fe=["textContent"],Ne=e("span",{class:"text-gray-600"},"this hour",-1),Me=["textContent"],je=e("span",{class:"text-gray-600"},"today",-1),ze={setup(s){const a=k(),t=m({recent:{this_minute:"...",this_hour:"...",today:"..."},failed:{this_minute:"...",this_hour:"...",today:"..."}});return A(()=>b(this,null,function*(){t.value=yield I({endpoint:"dashboard",router:a})})),(r,o)=>{const l=S("router-link");return d(),_(w,null,[Se,e("div",Ae,[h(l,{to:{name:"recent",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_minute)},null,8,Ie),Le]}),_:1},8,["to"]),h(l,{to:{name:"recent",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_hour)},null,8,Re),Pe]}),_:1},8,["to"]),h(l,{to:{name:"recent"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_day)},null,8,Ue),De]}),_:1})]),Ee,e("div",Ve,[h(l,{to:{name:"failed",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_minute)},null,8,Oe),Be]}),_:1},8,["to"]),h(l,{to:{name:"failed",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_hour)},null,8,Fe),Ne]}),_:1},8,["to"]),h(l,{to:{name:"failed"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_day)},null,8,Me),je]}),_:1})])],64)}}};const H={props:{status:String,classes:{type:Array,default:[]}},setup(s){function a(t){return t.charAt(0).toUpperCase()+t.slice(1)}return(t,r)=>(d(),_("span",{class:g(["px-2 inline-flex text-xs leading-5 font-semibold rounded-full",[`task-${s.status}`,...s.classes]])},p(a(s.status)),3))}},He={},Ke=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},[e("svg",{class:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[e("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"indigo","stroke-width":"4"}),e("path",{class:"opacity-75",fill:"indigo",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})])])],-1),Qe=[Ke];function Ge(s,a){return d(),_("tbody",null,Qe)}var We=T(He,[["render",Ge]]);const Je=e("label",{for:"queue",class:"block mb-2 font-medium"},"Queue",-1),Xe=["onKeyup"],Ye=e("label",{for:"status",class:"block mb-2 mt-6 font-medium"},"Status",-1),Ze=ae('',6),et=[Ze],tt={props:{focus:String},setup(s){const a=s,t=k(),r=M(),o=m(!1),l=m(null),n=m(null);function i(){t.push({name:r.name,query:C(C({},l.value.value?{queue:l.value.value}:{}),n.value?{status:n.value}:{})})}function c(u){u===""&&i()}return A(()=>{setTimeout(()=>o.value=!0),a.focus==="queue"&&l.value.focus()}),(u,f)=>(d(),_("div",{class:g(["w-[300px] fixed transition-transform right-0 top-0 p-6 px-6 shadow-2xl h-screen bg-white",{"translate-x-[300px]":o.value===!1}])},[Je,e("input",{type:"text",name:"queue",id:"queue",ref_key:"queue",ref:l,class:"bg-white py-2 px-3 w-full rounded border",onKeyup:[B(i,["enter"]),f[0]||(f[0]=v=>c(v.target.value))]},null,40,Xe),Ye,O(e("select",{name:"status",id:"status","onUpdate:modelValue":f[1]||(f[1]=v=>n.value=v),class:"bg-white py-2 px-3 w-full rounded border"},et,512),[[oe,n.value]]),e("button",{class:"bg-indigo-500 w-full mt-4 text-indigo-100 rounded py-2",onClick:i}," Apply Filter (or Press Enter) ")],2))}};const st={class:"text-4xl mb-2"},ot={class:"text-lg"},at={class:"flex flex-row mt-6"},nt={class:"flex-1"},lt={class:"align-middle"},rt={class:"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"},it={class:"table-fixed divide-y divide-gray-200 w-full"},ct={class:"bg-gray-50"},ut=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[50px]"}," # ",-1),dt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider max-w-xl w-[300px]"}," Name ",-1),pt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[100px]"}," Status ",-1),_t=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[150px] text-center"}," Attempts ",-1),ht=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[200px]"}," Created ",-1),mt={scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"},ft=y(" Queue "),xt={class:"inline relative"},vt=e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"},null,-1),gt=[vt],yt=e("th",{scope:"col",class:"relative px-6 py-3"},[e("span",{class:"sr-only"},"Edit")],-1),bt={key:1},wt=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},"No results.")],-1),kt=[wt],$t={class:"bg-white divide-y divide-gray-200"},Ct=["onClick"],qt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-900"},Tt={class:"px-6 py-4 whitespace-nowrap text-ellipsis text-sm text-gray-900"},St={class:"px-6 py-4 whitespace-nowrap"},At={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center"},It={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Lt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Rt=e("td",{class:"px-6 py-4 whitespace-nowrap text-right text-sm font-medium"},[e("a",{href:"#",class:"text-indigo-600 hover:text-indigo-900"},"View")],-1),R={props:{title:String,description:String,tasks:Array},setup(s){const a=s,t=m([]),r=m([]),o=m({visible:!1,focus:null});function l(n){t.value.push(n.id),setTimeout(()=>{t.value.splice(t.value.indexOf(n.id),1)},1e3)}return ne(()=>a.tasks,(n,i)=>{var c;if(!!i){r.value=[],i.map((u,f)=>{r[u.id]=f});for(const u of n)(r[u.id]===void 0||((c=i[r[u.id]])==null?void 0:c.status)!==u.status)&&l(u)}}),(n,i)=>(d(),_(w,null,[e("h1",st,p(s.title),1),e("p",ot,p(s.description),1),e("div",at,[e("div",nt,[e("div",lt,[e("div",rt,[e("table",it,[e("thead",ct,[e("tr",null,[ut,dt,pt,_t,ht,e("th",mt,[ft,e("div",xt,[(d(),_("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4 inline transition-transform hover:scale-[1.1] cursor-pointer",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",onClick:i[0]||(i[0]=()=>{o.value.visible=!o.value.visible,o.value.focus=o.value.visible?"queue":null})},gt))])]),yt])]),s.tasks===null?(d(),q(We,{key:0})):$("",!0),s.tasks&&s.tasks.length===0?(d(),_("tbody",bt,kt)):$("",!0),e("tbody",$t,[(d(!0),_(w,null,j(s.tasks,c=>(d(),_("tr",{class:g(["cursor-pointer hover:bg-indigo-100/10 transition-colors",{"bg-blue-300/30":t.value.includes(c.id)}]),onClick:u=>n.$router.push({name:`${n.$route.name}-task`,params:{uuid:c.uuid}})},[e("td",qt,p(c.id),1),e("td",Tt,p(c.name.substring(0,30))+p(c.name.length>30?"...":""),1),e("td",St,[h(H,{status:c.status},null,8,["status"])]),e("td",At,p(c.attempts),1),e("td",It,p(c.created),1),e("td",Lt,p(c.queue),1),Rt],10,Ct))),256))])])])])])]),o.value.visible?(d(),q(tt,{key:0,visible:o.value.visible,focus:o.value.focus},null,8,["visible","focus"])):$("",!0)],64))}},Pt={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{filter:"recent"},t),(r,o)=>(d(),q(R,{title:"Recent tasks",description:"Tasks that have been added or processed in the queue recently.",tasks:a.value},null,8,["tasks"]))}},Ut={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{status:"queued"},t),(r,o)=>(d(),q(R,{title:"Queued tasks",description:"Tasks that have been added to the queue recently.",tasks:a.value},null,8,["tasks"]))}},Dt={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{filter:"failed"},t),(r,o)=>(d(),q(R,{title:"Failed tasks",description:"Tasks that permanently failed after they have reached their max number of attempts.",tasks:a.value},null,8,["tasks"]))}};const Et={class:"absolute flex items-center justify-center w-2 h-2 bg-gray-200 rounded-full -left-1 ring-1 mt-3 ring-white"},Vt={props:{status:String,classes:{type:Array,default:[]}},setup(s){return(a,t)=>(d(),_("span",Et))}};var Ot=T(Vt,[["__scopeId","data-v-35155177"]]);const P=s=>(F("data-v-d792a216"),s=s(),N(),s),Bt={class:"text-4xl mb-2"},Ft={class:"flex"},Nt={class:"basis-[400px] shrink-0 pr-6 w-2/12"},Mt={class:"flex-initial sticky ml-4 mt-12"},jt={class:"relative border-l border-gray-200 dark:border-gray-700"},zt={class:"text-gray-900"},Ht={key:0,class:"bg-blue-100 text-blue-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Kt={key:0},Qt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Gt={class:"block mb-2 mt-2 text-xs text-black/70 font-normal leading-none"},Wt={class:"cursor-default"},Jt={class:"basis-auto overflow-x-auto pr-12"},Xt=P(()=>e("h2",{class:"text-2xl"},"Task Exception",-1)),Yt={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},Zt={key:1,class:"mt-12"},es=P(()=>e("h2",{class:"text-2xl"},"Task Payload",-1)),ts={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},ss=P(()=>e("div",{class:"basis-[250px] shrink-0 px-6"},[e("h2",{class:"text-3xl"},"Actions"),e("button",{class:"bg-gray-200 text-black/20 cursor-not-allowed mt-4 w-full rounded px-4 py-2"}," Retry "),e("span",{class:"text-xs text-gray-800 mt-2 inline-block"},"Retrying tasks is not available yet.")],-1)),os={setup(s){const a=M(),t=k(),r=m({id:null,status:"loading"});A(()=>b(this,null,function*(){r.value=yield I({endpoint:`task/${a.params.uuid}`,router:t})}));const o={scheduled:"Scheduled",queued:"Added to the queue",running:"Running",successful:"Successful",error:"An error occurred",failed:"Failed permanently"};return(l,n)=>{const i=S("Popper");return d(),_(w,null,[e("h1",Bt,"Task #"+p(r.value.id),1),h(H,{status:r.value.status,classes:["text-sm"]},null,8,["status"]),e("div",Ft,[e("div",Nt,[e("div",Mt,[e("ol",jt,[(d(!0),_(w,null,j(r.value.events,(c,u)=>(d(),_("li",{class:g(["ml-10 pt-1 mb-6",[`event-${c.status}`]])},[h(Ot,{status:c.status},null,8,["status"]),e("h3",zt,[y(p(o[c.status]||c.status)+" ",1),e("div",null,[c.queue?(d(),_("span",Ht,p(r.value.queue),1)):$("",!0)]),c.scheduled_at?(d(),_("div",Kt,[e("span",Qt," Scheduled: "+p(c.scheduled_at)+" (UTC) ",1)])):$("",!0)]),h(i,{content:c.datetime,hover:!0,arrow:!0,placement:"right"},{default:x(()=>[e("time",Gt,[e("span",Wt,p(c.diff),1)])]),_:2},1032,["content"])],2))),256))])])]),e("div",Jt,[r.value.exception?(d(),_(w,{key:0},[Xt,e("pre",Yt,p(r.value.exception),1)],64)):$("",!0),r.value.payload?(d(),_("div",Zt,[es,e("pre",ts,p(r.value.payload),1)])):$("",!0)]),ss])],64)}}};var U=T(os,[["__scopeId","data-v-d792a216"]]);const as=[{name:"home",path:"/",component:ze},{name:"login",path:"/login",component:Te},{name:"recent",path:"/recent",component:Pt,meta:{route:"recent"}},{name:"recent-task",path:"/recent/:uuid",component:U,meta:{route:"recent"}},{name:"queued",path:"/queued",component:Ut,meta:{route:"queued"}},{name:"queued-task",path:"/queued/:uuid",component:U,meta:{route:"queued"}},{name:"failed",path:"/failed",component:Dt,meta:{route:"failed"}},{name:"failed-task",path:"/failed/:uuid",component:U,meta:{route:"failed"}}];let K=null;"CloudTasks"in window&&(K=`/${window.CloudTasks.path}`);const Q=le({history:re(K),routes:as});Q.beforeEach((s,a,t)=>!localStorage.hasOwnProperty("cloud-tasks-token")&&s.name!=="login"?t({name:"login"}):t());ie(be).use(Q).component("Popper",ce).mount("#app"); +var W=Object.defineProperty,J=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var D=Object.getOwnPropertySymbols;var Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var E=(s,a,t)=>a in s?W(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t,C=(s,a)=>{for(var t in a||(a={}))Y.call(a,t)&&E(s,t,a[t]);if(D)for(var t of D(a))Z.call(a,t)&&E(s,t,a[t]);return s},V=(s,a)=>J(s,X(a));var w=(s,a,t)=>new Promise((r,o)=>{var l=i=>{try{c(t.next(i))}catch(u){o(u)}},n=i=>{try{c(t.throw(i))}catch(u){o(u)}},c=i=>i.done?r(i.value):Promise.resolve(i.value).then(l,n);c((t=t.apply(s,a)).next())});import{r as S,o as d,c as _,a as h,w as x,n as y,F as k,b as g,d as e,e as ee,f as te,u as $,g as m,h as A,i as O,v as se,j as B,p as F,k as N,t as p,l as M,m as oe,q as ae,s as ne,x as q,y as b,z as j,A as le,B as re,C as ie,D as ce}from"./vendor.433de25e.js";const ue=function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const n of l.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&r(n)}).observe(document,{childList:!0,subtree:!0});function t(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerpolicy&&(l.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?l.credentials="include":o.crossorigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(o){if(o.ep)return;o.ep=!0;const l=t(o);fetch(o.href,l)}};ue();var T=(s,a)=>{const t=s.__vccOpts||s;for(const[r,o]of a)t[r]=o;return t};const de={},pe=g("Dashboard "),_e=g("Recent "),he=g("Queued "),me=g("Failed ");function fe(s,a){var r,o,l,n,c,i,u,f,v;const t=S("router-link");return d(),_(k,null,[h(t,{to:{name:"home"},class:"block p-4 rounded mb-2 cursor-pointer"},{default:x(()=>[pe]),_:1}),h(t,{to:{name:"recent"},class:y(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((l=(o=(r=s.$route)==null?void 0:r.matched[0])==null?void 0:o.meta)==null?void 0:l.route)==="recent"}])},{default:x(()=>[_e]),_:1},8,["class"]),h(t,{to:{name:"queued"},class:y(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((i=(c=(n=s.$route)==null?void 0:n.matched[0])==null?void 0:c.meta)==null?void 0:i.route)==="queued"}])},{default:x(()=>[he]),_:1},8,["class"]),h(t,{to:{name:"failed"},class:y(["block p-4 rounded mb-2",{"router-link-active":((v=(f=(u=s.$route)==null?void 0:u.matched[0])==null?void 0:f.meta)==null?void 0:v.route)==="failed"}])},{default:x(()=>[me]),_:1},8,["class"])],64)}var xe=T(de,[["render",fe]]);const ve={class:"flex"},ye={class:"basis-auto w-[250px] shrink-0 bg-white p-6 min-h-screen"},ge={class:"flex-1 max-w-[calc(100%-250px)] p-6"},be={setup(s){return(a,t)=>{const r=S("router-view");return d(),_("div",ve,[e("aside",ye,[h(xe)]),e("div",ge,[h(r)])])}}};function I(){return w(this,arguments,function*({endpoint:s,router:a,body:t=null,method:r="GET",login:o=!1}={}){const l=yield fetch(`/cloud-tasks-api/${s}`,V(C({method:r},t?{body:t}:{}),{headers:C({},o?{}:{Authorization:`Bearer ${localStorage.getItem("cloud-tasks-token")}`})}));return l.status===403&&!o&&(localStorage.removeItem("cloud-tasks-token"),a.push({name:"login"})),o?yield l.text():yield l.json()})}function R(r){return w(this,arguments,function*(s,a={},t){let o=!1;const l=function(i){return w(this,null,function*(){if(o)return;const u=new URL(window.location.href),f=new URLSearchParams(u.search);for(const[v,G]of Object.entries(a))f.append(v,G);o=!0,i.value=yield I({endpoint:`tasks?${f.toString()}`,router:t}),o=!1})};l(s);let n=setInterval(()=>l(s),3e3);ee(function(){setTimeout(()=>l(s))});const c=function(){document.visibilityState==="visible"?(l(s),clearInterval(n),n=setInterval(()=>l(s),3e3)):document.visibilityState==="hidden"&&clearInterval(n)};document.addEventListener("visibilitychange",c),te(()=>{clearInterval(n),document.removeEventListener("visibilitychange",c),o=!1})})}const z=s=>(F("data-v-59cd799c"),s=s(),N(),s),we={class:"block w-full h-full flex items-center justify-center"},ke=z(()=>e("h3",{class:"text-4xl"},"This application is password protected.",-1)),$e=["onKeyup","disabled"],Ce=z(()=>e("div",{class:"text-center mt-6 text-xl"},[g(" Press "),e("span",{class:"bg-blue-200 py-1 px-2 ml-2 mr-2 rounded text-blue-800"},"Enter"),g(" to log in. ")],-1)),qe={setup(s){const a=$(),t=m(null),r=m(""),o=m(""),l=m(!1),n=m(!1);A(()=>{t.value.focus()});function c(){return w(this,null,function*(){if(r.value===""||r.value===o.value)return;o.value=r.value;const i=new FormData;i.append("password",r.value),n.value=!0;const u=yield I({endpoint:"login",method:"POST",body:i,login:!0});n.value=!1,u?(localStorage.setItem("cloud-tasks-token",u),a.push({name:"home"})):(l.value=!0,setTimeout(()=>{l.value=!1},820),setTimeout(()=>t.value.focus(),50))})}return(i,u)=>(d(),_("div",we,[e("div",null,[ke,O(e("input",{type:"password",class:y(["w-full p-2 px-6 text-2xl font-light mt-8 text-center outline-none shadow rounded-full",{shake:l.value}]),onKeyup:B(c,["enter"]),"onUpdate:modelValue":u[0]||(u[0]=f=>r.value=f),disabled:n.value,ref_key:"inputRef",ref:t},null,42,$e),[[se,r.value]]),Ce])]))}};var Te=T(qe,[["__scopeId","data-v-59cd799c"]]);const Se=e("h3",{class:"text-3xl mb-4"},"All tasks",-1),Ae={class:"grid grid-cols-3 gap-4"},Ie=["textContent"],Re=e("span",{class:"text-gray-600"},"this minute",-1),Le=["textContent"],Pe=e("span",{class:"text-gray-600"},"this hour",-1),Ue=["textContent"],De=e("span",{class:"text-gray-600"},"today",-1),Ee=e("h3",{class:"text-3xl mb-4 mt-8"},"Failed tasks",-1),Ve={class:"grid grid-cols-3 gap-4"},Oe=["textContent"],Be=e("span",{class:"text-gray-600"},"this minute",-1),Fe=["textContent"],Ne=e("span",{class:"text-gray-600"},"this hour",-1),Me=["textContent"],je=e("span",{class:"text-gray-600"},"today",-1),ze={setup(s){const a=$(),t=m({recent:{this_minute:"...",this_hour:"...",today:"..."},failed:{this_minute:"...",this_hour:"...",today:"..."}});return A(()=>w(this,null,function*(){t.value=yield I({endpoint:"dashboard",router:a})})),(r,o)=>{const l=S("router-link");return d(),_(k,null,[Se,e("div",Ae,[h(l,{to:{name:"recent",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_minute)},null,8,Ie),Re]}),_:1},8,["to"]),h(l,{to:{name:"recent",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_hour)},null,8,Le),Pe]}),_:1},8,["to"]),h(l,{to:{name:"recent"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_day)},null,8,Ue),De]}),_:1})]),Ee,e("div",Ve,[h(l,{to:{name:"failed",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_minute)},null,8,Oe),Be]}),_:1},8,["to"]),h(l,{to:{name:"failed",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_hour)},null,8,Fe),Ne]}),_:1},8,["to"]),h(l,{to:{name:"failed"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_day)},null,8,Me),je]}),_:1})])],64)}}};const H={props:{status:String,classes:{type:Array,default:[]}},setup(s){function a(t){return t.charAt(0).toUpperCase()+t.slice(1)}return(t,r)=>(d(),_("span",{class:y(["px-2 inline-flex text-xs leading-5 font-semibold rounded-full",[`task-${s.status}`,...s.classes]])},p(a(s.status)),3))}},He={},Ke=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},[e("svg",{class:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[e("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"indigo","stroke-width":"4"}),e("path",{class:"opacity-75",fill:"indigo",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})])])],-1),Qe=[Ke];function Ge(s,a){return d(),_("tbody",null,Qe)}var We=T(He,[["render",Ge]]);const Je=e("label",{for:"queue",class:"block mb-2 font-medium"},"Queue",-1),Xe=["onKeyup"],Ye=e("label",{for:"status",class:"block mb-2 mt-6 font-medium"},"Status",-1),Ze=ae('',6),et=[Ze],tt={props:{focus:String},setup(s){const a=s,t=$(),r=M(),o=m(!1),l=m(null),n=m(null);function c(){t.push({name:r.name,query:C(C({},l.value.value?{queue:l.value.value}:{}),n.value?{status:n.value}:{})})}function i(u){u===""&&c()}return A(()=>{setTimeout(()=>o.value=!0),a.focus==="queue"&&l.value.focus()}),(u,f)=>(d(),_("div",{class:y(["w-[300px] fixed transition-transform right-0 top-0 p-6 px-6 shadow-2xl h-screen bg-white",{"translate-x-[300px]":o.value===!1}])},[Je,e("input",{type:"text",name:"queue",id:"queue",ref_key:"queue",ref:l,class:"bg-white py-2 px-3 w-full rounded border",onKeyup:[B(c,["enter"]),f[0]||(f[0]=v=>i(v.target.value))]},null,40,Xe),Ye,O(e("select",{name:"status",id:"status","onUpdate:modelValue":f[1]||(f[1]=v=>n.value=v),class:"bg-white py-2 px-3 w-full rounded border"},et,512),[[oe,n.value]]),e("button",{class:"bg-indigo-500 w-full mt-4 text-indigo-100 rounded py-2",onClick:c}," Apply Filter (or Press Enter) ")],2))}};const st={class:"text-4xl mb-2"},ot={class:"text-lg"},at={class:"flex flex-row mt-6"},nt={class:"flex-1"},lt={class:"align-middle"},rt={class:"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"},it={class:"table-fixed divide-y divide-gray-200 w-full"},ct={class:"bg-gray-50"},ut=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[50px]"}," # ",-1),dt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider max-w-xl w-[300px]"}," Name ",-1),pt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[100px]"}," Status ",-1),_t=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[150px] text-center"}," Attempts ",-1),ht=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[200px]"}," Created ",-1),mt={scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"},ft=g(" Queue "),xt={class:"inline relative"},vt=e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"},null,-1),yt=[vt],gt=e("th",{scope:"col",class:"relative px-6 py-3"},[e("span",{class:"sr-only"},"Edit")],-1),bt={key:1},wt=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},"No results.")],-1),kt=[wt],$t={class:"bg-white divide-y divide-gray-200"},Ct=["onClick"],qt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-900"},Tt={class:"px-6 py-4 whitespace-nowrap text-ellipsis text-sm text-gray-900"},St={class:"px-6 py-4 whitespace-nowrap"},At={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center"},It={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Rt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Lt=e("td",{class:"px-6 py-4 whitespace-nowrap text-right text-sm font-medium"},[e("a",{href:"#",class:"text-indigo-600 hover:text-indigo-900"},"View")],-1),L={props:{title:String,description:String,tasks:Array},setup(s){const a=s,t=m([]),r=m([]),o=m({visible:!1,focus:null});function l(n){t.value.push(n.id),setTimeout(()=>{t.value.splice(t.value.indexOf(n.id),1)},1e3)}return ne(()=>a.tasks,(n,c)=>{var i;if(!!c){r.value=[],c.map((u,f)=>{r[u.id]=f});for(const u of n)(r[u.id]===void 0||((i=c[r[u.id]])==null?void 0:i.status)!==u.status)&&l(u)}}),(n,c)=>(d(),_(k,null,[e("h1",st,p(s.title),1),e("p",ot,p(s.description),1),e("div",at,[e("div",nt,[e("div",lt,[e("div",rt,[e("table",it,[e("thead",ct,[e("tr",null,[ut,dt,pt,_t,ht,e("th",mt,[ft,e("div",xt,[(d(),_("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4 inline transition-transform hover:scale-[1.1] cursor-pointer",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",onClick:c[0]||(c[0]=()=>{o.value.visible=!o.value.visible,o.value.focus=o.value.visible?"queue":null})},yt))])]),gt])]),s.tasks===null?(d(),q(We,{key:0})):b("",!0),s.tasks&&s.tasks.length===0?(d(),_("tbody",bt,kt)):b("",!0),e("tbody",$t,[(d(!0),_(k,null,j(s.tasks,i=>(d(),_("tr",{class:y(["cursor-pointer hover:bg-indigo-100/10 transition-colors",{"bg-blue-300/30":t.value.includes(i.id)}]),onClick:u=>n.$router.push({name:`${n.$route.name}-task`,params:{uuid:i.uuid}})},[e("td",qt,p(i.id),1),e("td",Tt,p(i.name.substring(0,30))+p(i.name.length>30?"...":""),1),e("td",St,[h(H,{status:i.status},null,8,["status"])]),e("td",At,p(i.attempts),1),e("td",It,p(i.created),1),e("td",Rt,p(i.queue),1),Lt],10,Ct))),256))])])])])])]),o.value.visible?(d(),q(tt,{key:0,visible:o.value.visible,focus:o.value.focus},null,8,["visible","focus"])):b("",!0)],64))}},Pt={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{filter:"recent"},t),(r,o)=>(d(),q(L,{title:"Recent tasks",description:"Tasks that have been added or processed in the queue recently.",tasks:a.value},null,8,["tasks"]))}},Ut={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{status:"queued"},t),(r,o)=>(d(),q(L,{title:"Queued tasks",description:"Tasks that have been added to the queue recently.",tasks:a.value},null,8,["tasks"]))}},Dt={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{filter:"failed"},t),(r,o)=>(d(),q(L,{title:"Failed tasks",description:"Tasks that permanently failed after they have reached their max number of attempts.",tasks:a.value},null,8,["tasks"]))}};const Et={class:"absolute flex items-center justify-center w-2 h-2 bg-gray-200 rounded-full -left-1 ring-1 mt-3 ring-white"},Vt={props:{status:String,classes:{type:Array,default:[]}},setup(s){return(a,t)=>(d(),_("span",Et))}};var Ot=T(Vt,[["__scopeId","data-v-35155177"]]);const P=s=>(F("data-v-5df67d4c"),s=s(),N(),s),Bt={class:"text-4xl mb-2"},Ft={class:"flex"},Nt={class:"basis-[400px] shrink-0 pr-6 w-2/12"},Mt={class:"flex-initial sticky ml-4 mt-12"},jt={class:"relative border-l border-gray-200 dark:border-gray-700"},zt={class:"text-gray-900"},Ht={key:0,class:"bg-blue-100 text-blue-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Kt={key:0},Qt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Gt={key:1},Wt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Jt={class:"block mb-2 mt-2 text-xs text-black/70 font-normal leading-none"},Xt={class:"cursor-default"},Yt={class:"basis-auto overflow-x-auto pr-12"},Zt=P(()=>e("h2",{class:"text-2xl"},"Task Exception",-1)),es={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},ts={key:1,class:"mt-12"},ss=P(()=>e("h2",{class:"text-2xl"},"Task Payload",-1)),os={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},as=P(()=>e("div",{class:"basis-[250px] shrink-0 px-6"},[e("h2",{class:"text-3xl"},"Actions"),e("button",{class:"bg-gray-200 text-black/20 cursor-not-allowed mt-4 w-full rounded px-4 py-2"}," Retry "),e("span",{class:"text-xs text-gray-800 mt-2 inline-block"},"Retrying tasks is not available yet.")],-1)),ns={setup(s){const a=M(),t=$(),r=m({id:null,status:"loading"});A(()=>w(this,null,function*(){r.value=yield I({endpoint:`task/${a.params.uuid}`,router:t})}));const o={scheduled:"Scheduled",queued:"Added to the queue",running:"Running",successful:"Successful",error:"An error occurred",failed:"Failed permanently",released:"Released"};return(l,n)=>{const c=S("Popper");return d(),_(k,null,[e("h1",Bt,"Task #"+p(r.value.id),1),h(H,{status:r.value.status,classes:["text-sm"]},null,8,["status"]),e("div",Ft,[e("div",Nt,[e("div",Mt,[e("ol",jt,[(d(!0),_(k,null,j(r.value.events,(i,u)=>(d(),_("li",{class:y(["ml-10 pt-1 mb-6",[`event-${i.status}`]])},[h(Ot,{status:i.status},null,8,["status"]),e("h3",zt,[g(p(o[i.status]||i.status)+" ",1),e("div",null,[i.queue?(d(),_("span",Ht,p(r.value.queue),1)):b("",!0)]),i.scheduled_at?(d(),_("div",Kt,[e("span",Qt," Scheduled: "+p(i.scheduled_at)+" (UTC) ",1)])):b("",!0),i.delay?(d(),_("div",Gt,[e("span",Wt," Delay: "+p(i.delay)+" seconds ",1)])):b("",!0)]),h(c,{content:i.datetime,hover:!0,arrow:!0,placement:"right"},{default:x(()=>[e("time",Jt,[e("span",Xt,p(i.diff),1)])]),_:2},1032,["content"])],2))),256))])])]),e("div",Yt,[r.value.exception?(d(),_(k,{key:0},[Zt,e("pre",es,p(r.value.exception),1)],64)):b("",!0),r.value.payload?(d(),_("div",ts,[ss,e("pre",os,p(r.value.payload),1)])):b("",!0)]),as])],64)}}};var U=T(ns,[["__scopeId","data-v-5df67d4c"]]);const ls=[{name:"home",path:"/",component:ze},{name:"login",path:"/login",component:Te},{name:"recent",path:"/recent",component:Pt,meta:{route:"recent"}},{name:"recent-task",path:"/recent/:uuid",component:U,meta:{route:"recent"}},{name:"queued",path:"/queued",component:Ut,meta:{route:"queued"}},{name:"queued-task",path:"/queued/:uuid",component:U,meta:{route:"queued"}},{name:"failed",path:"/failed",component:Dt,meta:{route:"failed"}},{name:"failed-task",path:"/failed/:uuid",component:U,meta:{route:"failed"}}];let K=null;"CloudTasks"in window&&(K=`/${window.CloudTasks.path}`);const Q=le({history:re(K),routes:ls});Q.beforeEach((s,a,t)=>!localStorage.hasOwnProperty("cloud-tasks-token")&&s.name!=="login"?t({name:"login"}):t());ie(be).use(Q).component("Popper",ce).mount("#app"); diff --git a/dashboard/dist/crossword.png b/dashboard/dist/crossword.png deleted file mode 100644 index 2f9f1ad..0000000 Binary files a/dashboard/dist/crossword.png and /dev/null differ diff --git a/dashboard/dist/dot-grid.png b/dashboard/dist/dot-grid.png deleted file mode 100644 index ebcefe9..0000000 Binary files a/dashboard/dist/dot-grid.png and /dev/null differ diff --git a/dashboard/dist/index.html b/dashboard/dist/index.html index 5b5fcee..440a7b8 100644 --- a/dashboard/dist/index.html +++ b/dashboard/dist/index.html @@ -5,9 +5,9 @@ Vite App - + - +
diff --git a/dashboard/dist/manifest.json b/dashboard/dist/manifest.json index fedb792..53f0594 100644 --- a/dashboard/dist/manifest.json +++ b/dashboard/dist/manifest.json @@ -1,13 +1,13 @@ { "index.html": { - "file": "assets/index.5a46c6a0.js", + "file": "assets/index.ea68d73f.js", "src": "index.html", "isEntry": true, "imports": [ "_vendor.433de25e.js" ], "css": [ - "assets/index.1002db9a.css" + "assets/index.d8eef428.css" ] }, "_vendor.433de25e.js": { diff --git a/dashboard/dist/pw_maze_white.png b/dashboard/dist/pw_maze_white.png deleted file mode 100644 index 6646483..0000000 Binary files a/dashboard/dist/pw_maze_white.png and /dev/null differ diff --git a/dashboard/src/components/Status.vue b/dashboard/src/components/Status.vue index e542088..b1beb73 100644 --- a/dashboard/src/components/Status.vue +++ b/dashboard/src/components/Status.vue @@ -31,7 +31,7 @@ function ucfirst(input) { .task-queued, .task-scheduled { @apply bg-gray-100 text-gray-500 } -.task-running { +.task-running, .task-released { @apply bg-blue-100 text-blue-800 } diff --git a/dashboard/src/components/Task.vue b/dashboard/src/components/Task.vue index b8297ea..031dd73 100644 --- a/dashboard/src/components/Task.vue +++ b/dashboard/src/components/Task.vue @@ -27,6 +27,7 @@ const titles = { successful: 'Successful', error: 'An error occurred', failed: 'Failed permanently', + released: 'Released', } @@ -60,6 +61,13 @@ const titles = { Scheduled: {{ event['scheduled_at'] }} (UTC) +
+ + Delay: {{ event['delay'] }} seconds + +
- ./tests/ConfigTest.php - ./tests/TaskHandlerTest.php - ./tests/CloudTasksApiTest.php - ./tests/CloudTasksDashboardTest.php + ./tests diff --git a/src/CloudTasksApiFake.php b/src/CloudTasksApiFake.php index da3b56c..59a046a 100644 --- a/src/CloudTasksApiFake.php +++ b/src/CloudTasksApiFake.php @@ -91,4 +91,9 @@ public function assertTaskCreated(Closure $closure): void Assert::assertTrue($count > 0, 'Task was not created.'); } + + public function assertCreatedTaskCount(int $count): void + { + Assert::assertCount($count, $this->createdTasks); + } } diff --git a/src/CloudTasksConnector.php b/src/CloudTasksConnector.php index 79465fe..db81cd6 100644 --- a/src/CloudTasksConnector.php +++ b/src/CloudTasksConnector.php @@ -23,6 +23,6 @@ public function connect(array $config) }; } - return new CloudTasksQueue($config, app(CloudTasksClient::class)); + return new CloudTasksQueue($config, app(CloudTasksClient::class), $config['after_commit'] ?? null); } } diff --git a/src/CloudTasksJob.php b/src/CloudTasksJob.php index fc83cbe..7dcea27 100644 --- a/src/CloudTasksJob.php +++ b/src/CloudTasksJob.php @@ -3,14 +3,21 @@ namespace Stackkit\LaravelGoogleCloudTasksQueue; use Illuminate\Container\Container; -use Illuminate\Queue\Jobs\Job as LaravelJob; use Illuminate\Contracts\Queue\Job as JobContract; +use Illuminate\Queue\Jobs\Job as LaravelJob; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleasedAfterException; use function Safe\json_encode; class CloudTasksJob extends LaravelJob implements JobContract { - private array $job; - private ?int $attempts; + /** + * The Cloud Tasks raw job payload (request payload). + * + * @var array + */ + public array $job; + private ?int $maxTries; public ?int $retryUntil = null; @@ -24,9 +31,14 @@ public function __construct(array $job, CloudTasksQueue $cloudTasksQueue) $this->job = $job; $this->container = Container::getInstance(); $this->cloudTasksQueue = $cloudTasksQueue; - /** @var \stdClass $command */ - $command = unserialize($job['data']['command']); - $this->queue = $command->queue; + + $command = TaskHandler::getCommandProperties($job['data']['command']); + $this->queue = $command['queue'] ?? config('queue.connections.' .config('queue.default') . '.queue'); + } + + public function job() + { + return $this->job; } public function getJobId(): string @@ -46,12 +58,12 @@ public function getRawBody(): string public function attempts(): ?int { - return $this->attempts; + return $this->job['internal']['attempts']; } public function setAttempts(int $attempts): void { - $this->attempts = $attempts; + $this->job['internal']['attempts'] = $attempts; } public function setMaxTries(int $maxTries): void @@ -95,4 +107,28 @@ public function delete(): void $this->cloudTasksQueue->delete($this); } + + public function release($delay = 0) + { + parent::release(); + + $this->cloudTasksQueue->release($this, $delay); + + $properties = TaskHandler::getCommandProperties($this->job['data']['command']); + $connection = $properties['connection'] ?? config('queue.default'); + + // The package uses the JobReleasedAfterException provided by Laravel to grab + // the payload of the released job in tests to easily run and test a released + // job. Because the event is only accessible in Laravel 9.x, we create an + // identical event to hook into for Laravel versions older than 9.x + if (version_compare(app()->version(), '9.0.0', '<')) { + if (data_get($this->job, 'internal.errored')) { + app('events')->dispatch(new JobReleasedAfterException($connection, $this)); + } + } + + if (! data_get($this->job, 'internal.errored')) { + app('events')->dispatch(new JobReleased($connection, $this, $delay)); + } + } } diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index cdcbcf4..a77e09c 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -12,8 +12,9 @@ use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Queue\Queue as LaravelQueue; use Illuminate\Support\Str; -use function Safe\json_encode; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; use function Safe\json_decode; +use function Safe\json_encode; class CloudTasksQueue extends LaravelQueue implements QueueContract { @@ -24,10 +25,11 @@ class CloudTasksQueue extends LaravelQueue implements QueueContract public array $config; - public function __construct(array $config, CloudTasksClient $client) + public function __construct(array $config, CloudTasksClient $client, $dispatchAfterCommit = false) { $this->client = $client; $this->config = $config; + $this->dispatchAfterCommit = $dispatchAfterCommit; } /** @@ -42,6 +44,25 @@ public function size($queue = null) return 0; } + /** + * Fallback method for Laravel 6x and 7x + * + * @param \Closure|string|object $job + * @param string $payload + * @param string $queue + * @param \DateTimeInterface|\DateInterval|int|null $delay + * @param callable $callback + * @return mixed + */ + protected function enqueueUsing($job, $payload, $queue, $delay, $callback) + { + if (method_exists(parent::class, 'enqueueUsing')) { + return parent::enqueueUsing($job, $payload, $queue, $delay, $callback); + } + + return $callback($payload, $queue, $delay); + } + /** * Push a new job onto the queue. * @@ -52,9 +73,15 @@ public function size($queue = null) */ public function push($job, $data = '', $queue = null) { - $this->pushToCloudTasks($queue, $this->createPayload( - $job, $this->getQueue($queue), $data - )); + return $this->enqueueUsing( + $job, + $this->createPayload($job, $this->getQueue($queue), $data), + $queue, + null, + function ($payload, $queue) { + return $this->pushRaw($payload, $queue); + } + ); } /** @@ -63,11 +90,13 @@ public function push($job, $data = '', $queue = null) * @param string $payload * @param string|null $queue * @param array $options - * @return void + * @return string */ public function pushRaw($payload, $queue = null, array $options = []) { - $this->pushToCloudTasks($queue, $payload); + $delay = ! empty($options['delay']) ? $options['delay'] : 0; + + $this->pushToCloudTasks($queue, $payload, $delay); } /** @@ -81,9 +110,15 @@ public function pushRaw($payload, $queue = null, array $options = []) */ public function later($delay, $job, $data = '', $queue = null) { - $this->pushToCloudTasks($queue, $this->createPayload( - $job, $this->getQueue($queue), $data - ), $delay); + return $this->enqueueUsing( + $job, + $this->createPayload($job, $this->getQueue($queue), $data), + $queue, + $delay, + function ($payload, $queue, $delay) { + return $this->pushToCloudTasks($queue, $payload, $delay); + } + ); } /** @@ -92,7 +127,7 @@ public function later($delay, $job, $data = '', $queue = null) * @param string|null $queue * @param string $payload * @param \DateTimeInterface|\DateInterval|int $delay - * @return void + * @return string */ protected function pushToCloudTasks($queue, $payload, $delay = 0) { @@ -103,16 +138,25 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) $httpRequest = $this->createHttpRequest(); $httpRequest->setUrl($this->getHandler()); $httpRequest->setHttpMethod(HttpMethod::POST); - $httpRequest->setBody( - // Laravel 7+ jobs have a uuid, but Laravel 6 doesn't have it. - // Since we are using and expecting the uuid in some places - // we will add it manually here if it's not present yet. - $this->withUuid($payload) - ); + + // Laravel 7+ jobs have a uuid, but Laravel 6 doesn't have it. + // Since we are using and expecting the uuid in some places + // we will add it manually here if it's not present yet. + [$payload, $uuid] = $this->withUuid($payload); + + // Since 3.x tasks are released back onto the queue after an exception has + // been thrown. This means we lose the native [X-CloudTasks-TaskRetryCount] header + // value and need to manually set and update the number of times a task has been attempted. + $payload = $this->withAttempts($payload); + + $httpRequest->setBody($payload); $task = $this->createTask(); $task->setHttpRequest($httpRequest); + // The deadline for requests sent to the app. If the app does not respond by + // this deadline then the request is cancelled and the attempt is marked as + // a failure. Cloud Tasks will retry the task according to the RetryConfig. if (!empty($this->config['dispatch_deadline'])) { $task->setDispatchDeadline(new Duration(['seconds' => $this->config['dispatch_deadline']])); } @@ -128,9 +172,11 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) $createdTask = CloudTasksApi::createTask($queueName, $task); event((new TaskCreated)->queue($queue)->task($task)); + + return $uuid; } - private function withUuid(string $payload): string + private function withUuid(string $payload): array { /** @var array $decoded */ $decoded = json_decode($payload, true); @@ -139,6 +185,21 @@ private function withUuid(string $payload): string $decoded['uuid'] = (string) Str::uuid(); } + return [ + json_encode($decoded), + $decoded['uuid'], + ]; + } + + private function withAttempts(string $payload): string + { + /** @var array $decoded */ + $decoded = json_decode($payload, true); + + if (!isset($decoded['internal']['attempts'])) { + $decoded['internal']['attempts'] = 0; + } + return json_encode($decoded); } @@ -179,6 +240,17 @@ public function delete(CloudTasksJob $job): void CloudTasksApi::deleteTask($taskName); } + public function release(CloudTasksJob $job, int $delay = 0): void + { + $job->delete(); + + $payload = $job->getRawBody(); + + $options = ['delay' => $delay]; + + $this->pushRaw($payload, $job->getQueue(), $options); + } + private function createTask(): Task { return app(Task::class); diff --git a/src/CloudTasksServiceProvider.php b/src/CloudTasksServiceProvider.php index 07b3c6f..d22a281 100644 --- a/src/CloudTasksServiceProvider.php +++ b/src/CloudTasksServiceProvider.php @@ -8,6 +8,8 @@ use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\ServiceProvider as LaravelServiceProvider; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; use function Safe\file_get_contents; use function Safe\json_decode; @@ -126,7 +128,9 @@ private function registerRoutes(): void private function registerDashboard(): void { - app('events')->listen(TaskCreated::class, function (TaskCreated $event) { + $events = $this->app['events']; + + $events->listen(TaskCreated::class, function (TaskCreated $event) { if (CloudTasks::dashboardDisabled()) { return; } @@ -134,7 +138,7 @@ private function registerDashboard(): void DashboardService::make()->add($event->queue, $event->task); }); - app('events')->listen(JobFailed::class, function (JobFailed $event) { + $events->listen(JobFailed::class, function (JobFailed $event) { if (!$event->job instanceof CloudTasksJob) { return; } @@ -147,40 +151,58 @@ private function registerDashboard(): void ); }); - app('events')->listen(JobProcessing::class, function (JobProcessing $event) { - if (!CloudTasks::dashboardEnabled()) { + $events->listen(JobProcessing::class, function (JobProcessing $event) { + if (!$event->job instanceof CloudTasksJob) { return; } - if ($event->job instanceof CloudTasksJob) { + if (CloudTasks::dashboardEnabled()) { DashboardService::make()->markAsRunning($event->job->uuid()); } }); - app('events')->listen(JobProcessed::class, function (JobProcessed $event) { - if (!CloudTasks::dashboardEnabled()) { + $events->listen(JobProcessed::class, function (JobProcessed $event) { + if (!$event->job instanceof CloudTasksJob) { return; } - if ($event->job instanceof CloudTasksJob) { + data_set($event->job->job, 'internal.processed', true); + + if (CloudTasks::dashboardEnabled()) { DashboardService::make()->markAsSuccessful($event->job->uuid()); } }); - app('events')->listen(JobExceptionOccurred::class, function (JobExceptionOccurred $event) { - if (!CloudTasks::dashboardEnabled()) { + $events->listen(JobExceptionOccurred::class, function (JobExceptionOccurred $event) { + if (!$event->job instanceof CloudTasksJob) { + return; + } + + data_set($event->job->job, 'internal.errored', true); + + if (CloudTasks::dashboardEnabled()) { + DashboardService::make()->markAsError($event); + } + }); + + $events->listen(JobFailed::class, function ($event) { + if (!$event->job instanceof CloudTasksJob) { return; } - DashboardService::make()->markAsError($event); + if (CloudTasks::dashboardEnabled()) { + DashboardService::make()->markAsFailed($event); + } }); - app('events')->listen(JobFailed::class, function ($event) { - if (!CloudTasks::dashboardEnabled()) { + $events->listen(JobReleased::class, function (JobReleased $event) { + if (!$event->job instanceof CloudTasksJob) { return; } - DashboardService::make()->markAsFailed($event); + if (CloudTasks::dashboardEnabled()) { + DashboardService::make()->markAsReleased($event); + } }); } } diff --git a/src/DashboardService.php b/src/DashboardService.php index 49e34d2..8800e3d 100644 --- a/src/DashboardService.php +++ b/src/DashboardService.php @@ -9,6 +9,7 @@ use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Events\JobFailed; use Illuminate\Support\Facades\DB; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; use function Safe\json_decode; class DashboardService @@ -31,6 +32,12 @@ private function getTaskBody(Task $task): string public function add(string $queue, Task $task): void { + $uuid = $this->getTaskUuid($task); + + if (StackkitCloudTask::whereTaskUuid($uuid)->exists()) { + return; + } + $metadata = new TaskMetadata(); $metadata->payload = $this->getTaskBody($task); @@ -51,7 +58,7 @@ public function add(string $queue, Task $task): void DB::table('stackkit_cloud_tasks') ->insert([ - 'task_uuid' => $this->getTaskUuid($task), + 'task_uuid' => $uuid, 'name' => $this->getTaskName($task), 'queue' => $queue, 'payload' => $this->getTaskBody($task), @@ -79,6 +86,10 @@ public function markAsSuccessful(string $uuid): void { $task = StackkitCloudTask::findByUuid($uuid); + if ($task->status === 'released') { + return; + } + $task->status = 'successful'; $task->addMetadataEvent([ 'status' => $task->status, @@ -129,6 +140,23 @@ public function markAsFailed(JobFailed $event): void $task->save(); } + public function markAsReleased(JobReleased $event): void + { + /** @var CloudTasksJob $job */ + $job = $event->job; + + $task = StackkitCloudTask::findByUuid($job->uuid()); + + $task->status = 'released'; + $task->addMetadataEvent([ + 'status' => $task->status, + 'datetime' => now()->utc()->toDateTimeString(), + 'delay' => $event->delay, + ]); + + $task->save(); + } + private function getTaskName(Task $task): string { /** @var array $decode */ diff --git a/src/Events/JobReleased.php b/src/Events/JobReleased.php new file mode 100644 index 0000000..614e45b --- /dev/null +++ b/src/Events/JobReleased.php @@ -0,0 +1,46 @@ +job = $job; + $this->connectionName = $connectionName; + $this->delay = $delay; + } +} diff --git a/src/Events/JobReleasedAfterException.php b/src/Events/JobReleasedAfterException.php new file mode 100644 index 0000000..603fbe3 --- /dev/null +++ b/src/Events/JobReleasedAfterException.php @@ -0,0 +1,37 @@ +job = $job; + $this->connectionName = $connectionName; + } +} diff --git a/src/TaskCreated.php b/src/Events/TaskCreated.php similarity index 86% rename from src/TaskCreated.php rename to src/Events/TaskCreated.php index 96f0f45..a05f415 100644 --- a/src/TaskCreated.php +++ b/src/Events/TaskCreated.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Stackkit\LaravelGoogleCloudTasksQueue; +namespace Stackkit\LaravelGoogleCloudTasksQueue\Events; use Google\Cloud\Tasks\V2\Task; diff --git a/src/LogFake.php b/src/LogFake.php index d9a32c6..e4e86ff 100644 --- a/src/LogFake.php +++ b/src/LogFake.php @@ -68,4 +68,12 @@ public function assertLogged(string $message): void { PHPUnit::assertTrue(in_array($message, $this->loggedMessages), 'The message [' . $message . '] was not logged.'); } + + public function assertNotLogged(string $message): void + { + PHPUnit::assertTrue( + ! in_array($message, $this->loggedMessages), + 'The message [' . $message . '] was logged.' + ); + } } diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 9c1ba50..a535f30 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -5,8 +5,13 @@ use Google\Cloud\Tasks\V2\CloudTasksClient; use Google\Cloud\Tasks\V2\RetryConfig; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Queue\Jobs\Job; +use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; +use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; +use Safe\Exceptions\JsonException; use stdClass; use UnexpectedValueException; use function Safe\json_decode; @@ -38,9 +43,9 @@ public function __construct(CloudTasksClient $client) $this->client = $client; } - public function handle(?array $task = null): void + public function handle(?string $task = null): void { - $task = $task ?: $this->captureTask(); + $task = $this->captureTask($task); $this->loadQueueConnectionConfiguration($task); @@ -51,17 +56,59 @@ public function handle(?array $task = null): void $this->handleTask($task); } + /** + * @param string|array|null $task + * @return array + * @throws JsonException + */ + private function captureTask($task): array + { + $task = $task ?: (string) (request()->getContent()); + + try { + $array = json_decode($task, true); + } catch (JsonException $e) { + $array = []; + } + + $validator = validator([ + 'json' => $task, + 'task' => $array, + 'name_header' => request()->header('X-CloudTasks-Taskname'), + ], [ + 'json' => 'required|json', + 'task' => 'required|array', + 'task.data' => 'required|array', + 'name_header' => 'required|string', + ]); + + try { + $validator->validate(); + } catch (ValidationException $e) { + if (config('app.debug')) { + throw $e; + } else { + abort(404); + } + } + + return json_decode($task, true); + } + private function loadQueueConnectionConfiguration(array $task): void { /** * @var stdClass $command */ - $command = unserialize($task['data']['command']); + $command = self::getCommandProperties($task['data']['command']); $connection = $command->connection ?? config('queue.default'); - $this->config = array_merge( - (array) config("queue.connections.{$connection}"), - ['connection' => $connection] - ); + $baseConfig = config('queue.connections.' . $connection); + $config = (new CloudTasksConnector())->connect($baseConfig)->config; + + // The connection name from the config may not be the actual connection name + $config['connection'] = $connection; + + $this->config = $config; } private function setQueue(): void @@ -69,33 +116,22 @@ private function setQueue(): void $this->queue = new CloudTasksQueue($this->config, $this->client); } - /** - * @throws CloudTasksException - */ - private function captureTask(): array - { - $input = (string) (request()->getContent()); - - if (!$input) { - throw new CloudTasksException('Could not read incoming task'); - } - - $task = json_decode($input, true); - - if (!is_array($task)) { - throw new CloudTasksException('Could not decode incoming task'); - } - - return $task; - } - private function handleTask(array $task): void { $job = new CloudTasksJob($task, $this->queue); $this->loadQueueRetryConfig($job); - $job->setAttempts((int) request()->header('X-CloudTasks-TaskRetryCount')); + // If the task has a [X-CloudTasks-TaskRetryCount] header higher than 0, then + // we know the job was created using an earlier version of the package. This + // job does not have the attempts tracked internally yet. + $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount'); + if ($taskRetryCountHeader && (int) $taskRetryCountHeader > 0) { + $job->setAttempts((int) $taskRetryCountHeader); + } else { + $job->setAttempts($task['internal']['attempts']); + } + $job->setMaxTries($this->retryConfig->getMaxAttempts()); // If the job is being attempted again we also check if a @@ -120,7 +156,7 @@ private function handleTask(array $task): void $job->setAttempts($job->attempts() + 1); - app('queue.worker')->process($this->config['connection'], $job, new WorkerOptions()); + app('queue.worker')->process($this->config['connection'], $job, $this->getWorkerOptions()); } private function loadQueueRetryConfig(CloudTasksJob $job): void @@ -131,4 +167,28 @@ private function loadQueueRetryConfig(CloudTasksJob $job): void $this->retryConfig = CloudTasksApi::getRetryConfig($queueName); } + + public static function getCommandProperties(string $command): array + { + if (Str::startsWith($command, 'O:')) { + return (array) unserialize($command, ['allowed_classes' => false]); + } + + if (app()->bound(Encrypter::class)) { + return (array) unserialize(app(Encrypter::class)->decrypt($command), ['allowed_classes' => ['Illuminate\Support\Carbon']]); + } + + return []; + } + + public function getWorkerOptions(): WorkerOptions + { + $options = new WorkerOptions(); + + $prop = version_compare(app()->version(), '8.0.0', '<') ? 'delay' : 'backoff'; + + $options->$prop = $this->config['backoff'] ?? 0; + + return $options; + } } diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php index 4ca8578..6cb7b99 100644 --- a/tests/CloudTasksDashboardTest.php +++ b/tests/CloudTasksDashboardTest.php @@ -10,6 +10,7 @@ use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator; use Stackkit\LaravelGoogleCloudTasksQueue\StackkitCloudTask; use Tests\Support\FailingJob; +use Tests\Support\JobThatWillBeReleased; use Tests\Support\SimpleJob; class CloudTasksDashboardTest extends TestCase @@ -258,8 +259,7 @@ public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard() 'status' => 'queued', 'name' => SimpleJob::class, ]); - $payload = \Safe\json_decode($task->getMetadata()['payload'], true); - $this->assertSame($payload, $job->payload); + $this->assertSame($task->getMetadata()['payload'], $job->payload); } /** @@ -396,9 +396,9 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard() ); $job = $this->dispatch(new FailingJob()); - $job->run(); - $job->run(); - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); + $releasedJob->run(); // Assert $task = StackkitCloudTask::firstOrFail(); @@ -414,6 +414,68 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard() ); } + /** + * @test + */ + public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() + { + // Arrange + \Illuminate\Support\Carbon::setTestNow(now()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + (new RetryConfig())->setMaxAttempts(3) + ); + + $this->dispatch(new JobThatWillBeReleased())->run(); + + // Assert + $task = StackkitCloudTask::firstOrFail(); + $events = $task->getEvents(); + + $this->assertCount(3, $events); + $this->assertEquals( + [ + 'status' => 'released', + 'datetime' => now()->toDateTimeString(), + 'diff' => '1 second ago', + 'delay' => 0, + ], + $events[2] + ); + } + + /** + * @test + */ + public function job_release_delay_is_added_to_the_metadata() + { + // Arrange + \Illuminate\Support\Carbon::setTestNow(now()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + (new RetryConfig())->setMaxAttempts(3) + ); + + $this->dispatch(new JobThatWillBeReleased(15))->run(); + + // Assert + $task = StackkitCloudTask::firstOrFail(); + $events = $task->getEvents(); + + $this->assertCount(3, $events); + $this->assertEquals( + [ + 'status' => 'released', + 'datetime' => now()->toDateTimeString(), + 'diff' => '1 second ago', + 'delay' => 15, + ], + $events[2] + ); + } + /** * @test */ @@ -425,10 +487,17 @@ public function test_publish() // Act & Assert $expectedPublishBase = dirname(__DIR__); - $this->artisan('vendor:publish --tag=cloud-tasks --force') - ->expectsOutput('Copied File [' . $expectedPublishBase . '/config/cloud-tasks.php] To [/config/cloud-tasks.php]') - ->expectsOutput('Copied Directory [' . $expectedPublishBase . '/dashboard/dist] To [/public/vendor/cloud-tasks]') - ->expectsOutput('Publishing complete.'); + if (version_compare(app()->version(), '9.0.0', '>=')) { + $this->artisan('vendor:publish --tag=cloud-tasks --force') + ->expectsOutputToContain('Publishing [cloud-tasks] assets.') + ->expectsOutputToContain('Copying file [' . $expectedPublishBase . '/config/cloud-tasks.php] to [config/cloud-tasks.php]') + ->expectsOutputToContain('Copying directory [' . $expectedPublishBase . '/dashboard/dist] to [public/vendor/cloud-tasks]'); + } else { + $this->artisan('vendor:publish --tag=cloud-tasks --force') + ->expectsOutput('Copied File [' . $expectedPublishBase . '/config/cloud-tasks.php] To [/config/cloud-tasks.php]') + ->expectsOutput('Copied Directory [' . $expectedPublishBase . '/dashboard/dist] To [/public/vendor/cloud-tasks]') + ->expectsOutput('Publishing complete.'); + } } /** diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 0a89bda..4a019b9 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -5,10 +5,27 @@ namespace Tests; use Google\Cloud\Tasks\V2\HttpMethod; +use Google\Cloud\Tasks\V2\RetryConfig; use Google\Cloud\Tasks\V2\Task; +use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Queue\Events\JobQueued; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Queue; use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; +use Stackkit\LaravelGoogleCloudTasksQueue\LogFake; +use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator; +use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; use Tests\Support\FailingJob; +use Tests\Support\FailingJobWithExponentialBackoff; +use Tests\Support\JobThatWillBeReleased; use Tests\Support\SimpleJob; +use Tests\Support\User; +use Tests\Support\UserJob; class QueueTest extends TestCase { @@ -137,20 +154,328 @@ public function it_posts_the_task_the_correct_queue() // Assert CloudTasksApi::assertTaskCreated(function (Task $task, string $queueName): bool { $decoded = json_decode($task->getHttpRequest()->getBody(), true); - $command = unserialize($decoded['data']['command']); + $command = TaskHandler::getCommandProperties($decoded['data']['command']); return $decoded['displayName'] === SimpleJob::class - && $command->queue === null + && ($command['queue'] ?? null) === null && $queueName === 'projects/my-test-project/locations/europe-west6/queues/barbequeue'; }); CloudTasksApi::assertTaskCreated(function (Task $task, string $queueName): bool { $decoded = json_decode($task->getHttpRequest()->getBody(), true); - $command = unserialize($decoded['data']['command']); + $command = TaskHandler::getCommandProperties($decoded['data']['command']); return $decoded['displayName'] === FailingJob::class - && $command->queue === 'my-special-queue' + && $command['queue'] === 'my-special-queue' && $queueName === 'projects/my-test-project/locations/europe-west6/queues/my-special-queue'; }); } + + /** + * @test + */ + public function it_can_dispatch_after_commit_inline() + { + if (version_compare(app()->version(), '8.0.0', '<')) { + $this->markTestSkipped('Not supported by Laravel 7.x and below.'); + } + + // Arrange + CloudTasksApi::fake(); + Event::fake(); + + // Act & Assert + Event::assertNotDispatched(JobQueued::class); + DB::beginTransaction(); + SimpleJob::dispatch()->afterCommit(); + Event::assertNotDispatched(JobQueued::class); + while (DB::transactionLevel() !== 0) { + DB::commit(); + } + Event::assertDispatched(JobQueued::class, function (JobQueued $event) { + return $event->job instanceof SimpleJob; + }); + } + + /** + * @test + */ + public function it_can_dispatch_after_commit_through_config() + { + if (version_compare(app()->version(), '8.0.0', '<')) { + $this->markTestSkipped('Not supported by Laravel 7.x and below.'); + } + + // Arrange + CloudTasksApi::fake(); + Event::fake(); + $this->setConfigValue('after_commit', true); + + // Act & Assert + Event::assertNotDispatched(JobQueued::class); + DB::beginTransaction(); + SimpleJob::dispatch(); + Event::assertNotDispatched(JobQueued::class); + while (DB::transactionLevel() !== 0) { + DB::commit(); + } + Event::assertDispatched(JobQueued::class, function (JobQueued $event) { + return $event->job instanceof SimpleJob; + }); + } + + /** + * @test + */ + public function jobs_can_be_released() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake([ + $this->getJobReleasedAfterExceptionEvent(), + JobReleased::class, + ]); + + // Act + $this->dispatch(new JobThatWillBeReleased())->run(); + + // Assert + Event::assertNotDispatched($this->getJobReleasedAfterExceptionEvent()); + CloudTasksApi::assertDeletedTaskCount(1); + $releasedJob = null; + Event::assertDispatched(JobReleased::class, function (JobReleased $event) use (&$releasedJob) { + $releasedJob = $event->job; + return true; + }); + CloudTasksApi::assertTaskCreated(function (Task $task) { + $body = $task->getHttpRequest()->getBody(); + $decoded = json_decode($body, true); + return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased' + && $decoded['internal']['attempts'] === 1; + }); + + $this->runFromPayload($releasedJob->getRawBody()); + + CloudTasksApi::assertDeletedTaskCount(2); + CloudTasksApi::assertTaskCreated(function (Task $task) { + $body = $task->getHttpRequest()->getBody(); + $decoded = json_decode($body, true); + return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased' + && $decoded['internal']['attempts'] === 2; + }); + } + + /** + * @test + */ + public function jobs_can_be_released_with_a_delay() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake([ + $this->getJobReleasedAfterExceptionEvent(), + JobReleased::class, + ]); + Carbon::setTestNow(now()->addDay()); + + // Act + $this->dispatch(new JobThatWillBeReleased(15))->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + $body = $task->getHttpRequest()->getBody(); + $decoded = json_decode($body, true); + + $scheduleTime = $task->getScheduleTime() ? $task->getScheduleTime()->getSeconds() : null; + + return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased' + && $decoded['internal']['attempts'] === 1 + && $scheduleTime === now()->getTimestamp() + 15; + }); + } + + /** @test */ + public function test_default_backoff() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act + $this->dispatch(new FailingJob())->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return is_null($task->getScheduleTime()); + }); + } + + /** @test */ + public function test_backoff_from_queue_config() + { + // Arrange + Carbon::setTestNow(now()->addDay()); + $this->setConfigValue('backoff', 123); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act + $this->dispatch(new FailingJob())->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 123; + }); + } + + /** @test */ + public function test_backoff_from_job() + { + // Arrange + Carbon::setTestNow(now()->addDay()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act + $failingJob = new FailingJob(); + $prop = version_compare(app()->version(), '8.0.0', '<') ? 'delay' : 'backoff'; + $failingJob->$prop = 123; + $this->dispatch($failingJob)->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 123; + }); + } + + /** @test */ + public function test_exponential_backoff_from_job_method() + { + if (version_compare(app()->version(), '8.0.0', '<')) { + $this->markTestSkipped('Not supported by Laravel 7.x and below.'); + } + + // Arrange + Carbon::setTestNow(now()->addDay()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + + // Act + $releasedJob = $this->dispatch(new FailingJobWithExponentialBackoff()) + ->runAndGetReleasedJob(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); + $releasedJob->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 50; + }); + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 60; + }); + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 70; + }); + } + + /** @test */ + public function test_failing_method_on_job() + { + // Arrange + CloudTasksApi::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + (new RetryConfig())->setMaxAttempts(1) + ); + + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + // Act + $this->dispatch(new FailingJob())->run(); + + // Assert + Log::assertLogged('FailingJob:failed'); + } + + /** @test */ + public function test_queue_before_and_after_hooks() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + // Act + Queue::before(function (JobProcessing $event) { + logger('Queue::before:' . $event->job->payload()['data']['commandName']); + }); + Queue::after(function (JobProcessed $event) { + logger('Queue::after:' . $event->job->payload()['data']['commandName']); + }); + $this->dispatch(new SimpleJob())->run(); + + // Assert + Log::assertLogged('Queue::before:Tests\Support\SimpleJob'); + Log::assertLogged('Queue::after:Tests\Support\SimpleJob'); + } + + /** @test */ + public function test_queue_looping_hook_not_supported_with_this_package() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + // Act + Queue::looping(function () { + logger('Queue::looping'); + }); + $this->dispatch(new SimpleJob())->run(); + + // Assert + Log::assertNotLogged('Queue::looping'); + } + + /** @test */ + public function test_ignoring_jobs_with_deleted_models() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + $user1 = User::create([ + 'name' => 'John', + 'email' => 'johndoe@example.com', + 'password' => bcrypt('test'), + ]); + + $user2 = User::create([ + 'name' => 'Jane', + 'email' => 'janedoe@example.com', + 'password' => bcrypt('test'), + ]); + + // Act + $this->dispatch(new UserJob($user1))->runWithoutExceptionHandler(); + + $job = $this->dispatch(new UserJob($user2)); + $user2->delete(); + $job->runWithoutExceptionHandler(); + + // Act + Log::assertLogged('UserJob:John'); + CloudTasksApi::assertTaskDeleted($job->task->getName()); + } } diff --git a/tests/Support/EncryptedJob.php b/tests/Support/EncryptedJob.php new file mode 100644 index 0000000..8f8e4ff --- /dev/null +++ b/tests/Support/EncryptedJob.php @@ -0,0 +1,20 @@ +releaseDelay = $releaseDelay; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + logger('JobThatWillBeReleased:beforeRelease'); + $this->release($this->releaseDelay); + logger('JobThatWillBeReleased:afterRelease'); + } +} diff --git a/tests/Support/User.php b/tests/Support/User.php new file mode 100644 index 0000000..7ffec22 --- /dev/null +++ b/tests/Support/User.php @@ -0,0 +1,12 @@ +user = $user; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + logger('UserJob:' . $this->user->name); + } +} diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php index b70331f..36a2d13 100644 --- a/tests/TaskHandlerTest.php +++ b/tests/TaskHandlerTest.php @@ -5,21 +5,17 @@ use Firebase\JWT\ExpiredException; use Google\Cloud\Tasks\V2\RetryConfig; use Google\Protobuf\Duration; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Queue\Events\JobExceptionOccurred; -use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Queue; use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi; use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksException; -use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksJob; use Stackkit\LaravelGoogleCloudTasksQueue\LogFake; use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator; use Stackkit\LaravelGoogleCloudTasksQueue\StackkitCloudTask; +use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; +use Tests\Support\EncryptedJob; use Tests\Support\FailingJob; use Tests\Support\SimpleJob; use UnexpectedValueException; @@ -33,6 +29,115 @@ protected function setUp(): void CloudTasksApi::fake(); } + /** + * @test + * @testWith [true] + * [false] + */ + public function it_returns_responses_for_empty_payloads($debug) + { + // Arrange + config()->set('app.debug', $debug); + + // Act + $response = $this->postJson(action([TaskHandler::class, 'handle'])); + + // Assert + if ($debug) { + $response->assertJsonValidationErrors('task'); + } else { + $response->assertNotFound(); + } + } + + /** + * @test + * @testWith [true] + * [false] + */ + public function it_returns_responses_for_invalid_json($debug) + { + // Arrange + config()->set('app.debug', $debug); + + // Act + $response = $this->call( + 'POST', + action([TaskHandler::class, 'handle']), + [], + [], + [], + [ + 'HTTP_ACCEPT' => 'application/json', + ], + 'test', + ); + + // Assert + if ($debug) { + $response->assertJsonValidationErrors('task'); + $this->assertEquals('The json must be a valid JSON string.', $response->json('errors.json.0')); + } else { + $response->assertNotFound(); + } + } + + /** + * @test + * @testWith ["{\"invalid\": \"data\"}", "The task.data field is required."] + * ["{\"data\": \"\"}", "The task.data field is required."] + * ["{\"data\": \"test\"}", "The task.data must be an array."] + */ + public function it_returns_responses_for_invalid_payloads(string $payload, string $expectedMessage) + { + // Arrange + + // Act + $response = $this->call( + 'POST', + action([TaskHandler::class, 'handle']), + [], + [], + [], + [ + 'HTTP_ACCEPT' => 'application/json', + ], + $payload, + ); + + // Assert + $response->assertJsonValidationErrors('task.data'); + $this->assertEquals($expectedMessage, $response->json(['errors', 'task.data', 0])); + } + + /** + * @test + * @testWith [true] + * [false] + */ + public function it_validates_headers(bool $withHeaders) + { + // Arrange + $this->withExceptionHandling(); + + // Act + $response = $this->postJson( + action([TaskHandler::class, 'handle']), + [], + $withHeaders + ? [ + 'X-CloudTasks-Taskname' => 'MyTask', + ] : [] + ); + + // Assert + if ($withHeaders) { + $response->assertJsonMissingValidationErrors('name_header'); + } else { + $response->assertJsonValidationErrors('name_header'); + } + } + /** * @test */ @@ -132,13 +237,13 @@ public function after_max_attempts_it_will_log_to_failed_table() // Act & Assert $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob->run(); $this->assertDatabaseCount('failed_jobs', 1); } @@ -157,18 +262,18 @@ public function after_max_attempts_it_will_delete_the_task() $job = $this->dispatch(new FailingJob()); // Act & Assert - $job->run(); - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + $releasedJob = $job->runAndGetReleasedJob(); + CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + $releasedJob = $releasedJob->runAndGetReleasedJob(); + CloudTasksApi::assertDeletedTaskCount(2); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); - CloudTasksApi::assertDeletedTaskCount(1); + $releasedJob->run(); + CloudTasksApi::assertDeletedTaskCount(3); CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 1); } @@ -187,19 +292,19 @@ public function after_max_retry_until_it_will_log_to_failed_table_and_delete_the $job = $this->dispatch(new FailingJob()); // Act - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); // Assert - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); // Act CloudTasksApi::partialMock()->shouldReceive('getRetryUntilTimestamp')->andReturn(1); - $job->run(); + $releasedJob->run(); // Assert - CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertDeletedTaskCount(2); CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 1); } @@ -220,8 +325,8 @@ public function test_unlimited_max_attempts() $job = $this->dispatch(new FailingJob()); foreach (range(1, 50) as $attempt) { $job->run(); - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + CloudTasksApi::assertDeletedTaskCount($attempt); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); } } @@ -246,15 +351,15 @@ public function test_max_attempts_in_combination_with_retry_until() $job = $this->dispatch(new FailingJob()); // Act & Assert - $job->run(); - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); # After 2 attempts both Laravel versions should report the same: 2 errors and 0 failures. - $task = StackkitCloudTask::whereTaskUuid($job->payload['uuid'])->firstOrFail(); + $task = StackkitCloudTask::whereTaskUuid($job->payloadAsArray('uuid'))->firstOrFail(); $this->assertEquals(2, $task->getNumberOfAttempts()); $this->assertEquals('error', $task->status); - $job->run(); + $releasedJob->run(); # Max attempts was reached # Laravel 5, 6, 7: fail because max attempts was reached @@ -268,8 +373,108 @@ public function test_max_attempts_in_combination_with_retry_until() } CloudTasksApi::shouldReceive('getRetryUntilTimestamp')->andReturn(time() - 1); - $job->run(); + $releasedJob->run(); $this->assertEquals('failed', $task->fresh()->status); } + + /** + * @test + */ + public function it_can_handle_encrypted_jobs() + { + if (version_compare(app()->version(), '8.0.0', '<')) { + $this->markTestSkipped('Not supported by Laravel 7.x and below.'); + } + + // Arrange + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + // Act + $job = $this->dispatch(new EncryptedJob()); + $job->run(); + + // Assert + $this->assertStringContainsString( + 'O:26:"Tests\Support\EncryptedJob"', + decrypt($job->payloadAsArray('data.command')), + ); + + Log::assertLogged('EncryptedJob:success'); + } + + /** + * @test + */ + public function failing_jobs_are_released() + { + // Arrange + OpenIdVerificator::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + (new RetryConfig())->setMaxAttempts(3) + ); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act + $job = $this->dispatch(new FailingJob()); + + CloudTasksApi::assertDeletedTaskCount(0); + CloudTasksApi::assertCreatedTaskCount(1); + CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + + $job->run(); + + CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertCreatedTaskCount(2); + CloudTasksApi::assertTaskDeleted($job->task->getName()); + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) { + return $event->job->attempts() === 1; + }); + } + + /** + * @test + */ + public function attempts_are_tracked_internally() + { + // Arrange + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act & Assert + $job = $this->dispatch(new FailingJob()); + $job->run(); + $releasedJob = null; + + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) use (&$releasedJob) { + $releasedJob = $event->job->getRawBody(); + return $event->job->attempts() === 1; + }); + + $this->runFromPayload($releasedJob); + + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) { + return $event->job->attempts() === 2; + }); + } + + /** + * @test + */ + public function attempts_are_copied_from_x_header() + { + // Arrange + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act & Assert + $job = $this->dispatch(new FailingJob()); + request()->headers->set('X-CloudTasks-TaskRetryCount', 6); + $job->run(); + + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) { + return $event->job->attempts() === 7; + }); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 17e70f3..9110869 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,15 +5,14 @@ use Closure; use Firebase\JWT\JWT; use Google\ApiCore\ApiException; -use Google\Cloud\Tasks\V2\Queue; -use Google\Cloud\Tasks\V2\RetryConfig; +use Google\Cloud\Tasks\V2\CloudTasksClient; use Google\Cloud\Tasks\V2\Task; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Support\Facades\DB; -use Google\Cloud\Tasks\V2\CloudTasksClient; use Illuminate\Support\Facades\Event; -use Mockery; -use Stackkit\LaravelGoogleCloudTasksQueue\TaskCreated; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleasedAfterException as PackageJobReleasedAfterException; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; class TestCase extends \Orchestra\Testbench\TestCase @@ -25,6 +24,8 @@ class TestCase extends \Orchestra\Testbench\TestCase */ public $client; + public string $releasedJobPayload; + protected function setUp(): void { parent::setUp(); @@ -32,6 +33,13 @@ protected function setUp(): void $this->withFactories(__DIR__ . '/../factories'); $this->defaultHeaders['Authorization'] = 'Bearer ' . encrypt(time() + 10); + + Event::listen( + $this->getJobReleasedAfterExceptionEvent(), + function ($event) { + $this->releasedJobPayload = $event->job->getRawBody(); + } + ); } /** @@ -116,10 +124,12 @@ protected function setConfigValue($key, $value) public function dispatch($job) { $payload = null; + $payloadAsArray = []; $task = null; - Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$task) { - $payload = json_decode($event->task->getHttpRequest()->getBody(), true); + Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$payloadAsArray, &$task) { + $payload = $event->task->getHttpRequest()->getBody(); + $payloadAsArray = json_decode($payload, true); $task = $event->task; request()->headers->set('X-Cloudtasks-Taskname', $task->getName()); @@ -127,14 +137,16 @@ public function dispatch($job) dispatch($job); - return new class($payload, $task) { - public array $payload = []; + return new class($payload, $task, $this) { + public string $payload; public Task $task; + public TestCase $testCase; - public function __construct(array $payload, Task $task) + public function __construct(string $payload, Task $task, TestCase $testCase) { $this->payload = $payload; $this->task = $task; + $this->testCase = $testCase; } public function run(): void @@ -142,33 +154,42 @@ public function run(): void rescue(function (): void { app(TaskHandler::class)->handle($this->payload); }); - - $taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', 0); - request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1); } public function runWithoutExceptionHandler(): void { app(TaskHandler::class)->handle($this->payload); + } + + public function runAndGetReleasedJob(): self + { + rescue(function (): void { + app(TaskHandler::class)->handle($this->payload); + }); + + return new self( + $this->testCase->releasedJobPayload, + $this->task, + $this->testCase + ); + } - $taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', 0); - request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1); + public function payloadAsArray(string $key = '') + { + $decoded = json_decode($this->payload, true); + + return data_get($decoded, $key ?: null); } }; } - public function runFromPayload(array $payload): void + public function runFromPayload(string $payload): void { rescue(function () use ($payload) { app(TaskHandler::class)->handle($payload); }); } - public function dispatchAndRun($job): void - { - $this->runFromPayload($this->dispatch($job)); - } - public function assertTaskDeleted(string $taskId): void { try { @@ -214,4 +235,14 @@ protected function assertDatabaseCount($table, int $count, $connection = null) { $this->assertEquals($count, DB::connection($connection)->table($table)->count()); } + + public function getJobReleasedAfterExceptionEvent(): string + { + // The JobReleasedAfterException event is not available in Laravel versions + // below 9.x so instead for those versions we throw our own event which + // is identical to the Laravel one. + return version_compare(app()->version(), '9.0.0', '<') + ? PackageJobReleasedAfterException::class + : JobReleasedAfterException::class; + } }