tina4ruby 3.13.94 → 3.13.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +883 -0
- data/README.md +1 -1
- data/lib/tina4/auth.rb +166 -87
- data/lib/tina4/auto_crud.rb +29 -32
- data/lib/tina4/cache_backends/base_backend.rb +19 -0
- data/lib/tina4/cache_backends/database_backend.rb +29 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +124 -13
- data/lib/tina4/cache_backends/memory_backend.rb +15 -0
- data/lib/tina4/cache_backends/redis_backend.rb +173 -52
- data/lib/tina4/cache_backends.rb +10 -1
- data/lib/tina4/cli.rb +23 -39
- data/lib/tina4/cors.rb +186 -30
- data/lib/tina4/database/sqlite3_adapter.rb +4 -1
- data/lib/tina4/database.rb +322 -22
- data/lib/tina4/database_adapter.rb +178 -0
- data/lib/tina4/database_result.rb +63 -17
- data/lib/tina4/database_url.rb +363 -0
- data/lib/tina4/dev.rb +0 -1
- data/lib/tina4/dev_admin.rb +118 -20
- data/lib/tina4/dispatch_pipeline.rb +605 -0
- data/lib/tina4/docstore.rb +274 -60
- data/lib/tina4/drivers/firebird_driver.rb +118 -4
- data/lib/tina4/drivers/mongodb_driver.rb +19 -4
- data/lib/tina4/drivers/mssql_driver.rb +73 -10
- data/lib/tina4/drivers/mysql_driver.rb +71 -4
- data/lib/tina4/drivers/odbc_driver.rb +40 -4
- data/lib/tina4/drivers/postgres_driver.rb +97 -10
- data/lib/tina4/drivers/sqlite_driver.rb +21 -2
- data/lib/tina4/env.rb +176 -34
- data/lib/tina4/field_types.rb +12 -0
- data/lib/tina4/health.rb +30 -14
- data/lib/tina4/job.rb +15 -5
- data/lib/tina4/log.rb +236 -32
- data/lib/tina4/mcp.rb +11 -5
- data/lib/tina4/messenger.rb +248 -36
- data/lib/tina4/metrics.rb +179 -891
- data/lib/tina4/middleware.rb +191 -56
- data/lib/tina4/migration.rb +17 -1
- data/lib/tina4/orm.rb +114 -17
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/queue.rb +154 -9
- data/lib/tina4/queue_backends/kafka_backend.rb +191 -2
- data/lib/tina4/queue_backends/lite_backend.rb +121 -25
- data/lib/tina4/queue_backends/mongo_backend.rb +146 -10
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +208 -1
- data/lib/tina4/rack_app.rb +94 -316
- data/lib/tina4/request.rb +48 -8
- data/lib/tina4/response.rb +42 -1
- data/lib/tina4/response_cache.rb +142 -24
- data/lib/tina4/router.rb +141 -12
- data/lib/tina4/session.rb +256 -33
- data/lib/tina4/session_handlers/database_handler.rb +185 -20
- data/lib/tina4/session_handlers/file_handler.rb +113 -21
- data/lib/tina4/session_handlers/memcached_handler.rb +183 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +232 -15
- data/lib/tina4/session_handlers/mongo_wire_client.rb +300 -0
- data/lib/tina4/session_handlers/redis_handler.rb +20 -6
- data/lib/tina4/session_handlers/valkey_handler.rb +18 -4
- data/lib/tina4/shutdown.rb +180 -30
- data/lib/tina4/sql_translator.rb +110 -0
- data/lib/tina4/swagger.rb +50 -18
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -6
- data/lib/tina4.rb +289 -37
- metadata +35 -17
- data/lib/tina4/scss_compiler.rb +0 -349
|
@@ -1 +1 @@
|
|
|
1
|
-
*,*::before,*::after{box-sizing:border-box}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;scroll-behavior:smooth}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.25}p{margin-top:0;margin-bottom:1rem}ol,ul{padding-left:2rem;margin-top:0;margin-bottom:1rem}a{color:#4a90d9;text-decoration:none}a:hover{color:#256ab1;text-decoration:underline}img,svg{max-width:100%;height:auto;vertical-align:middle}table{border-collapse:collapse;caption-side:bottom}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;cursor:pointer}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid rgba(0,0,0,.1);opacity:.25}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}[hidden]{display:none !important}h1{font-size:2.25rem}h2{font-size:1.875rem}h3{font-size:1.5rem}h4{font-size:1.25rem}h5{font-size:1.125rem}h6{font-size:1rem}small,.small{font-size:.875em}mark,.mark{padding:.2em;background-color:rgba(255,193,7,.3)}blockquote{margin:0 0 1rem;padding:.5rem 1rem;border-left:.25rem solid rgba(0,0,0,.1);font-size:1.125rem}code{font-size:.875em;color:#dc3545;word-wrap:break-word}pre{display:block;padding:1rem;font-size:.875em;color:#212529;background-color:#f8f9fa;border-radius:.25rem}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.container,.container-fluid{width:100%;padding-right:.75rem;padding-left:.75rem;margin-right:auto;margin-left:auto}@media(min-width: 576px){.container{max-width:540px}}@media(min-width: 768px){.container{max-width:720px}}@media(min-width: 992px){.container{max-width:960px}}@media(min-width: 1200px){.container{max-width:1140px}}@media(min-width: 1400px){.container{max-width:1320px}}.row{display:flex;flex-wrap:wrap;margin-right:-0.75rem;margin-left:-0.75rem}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:.75rem;padding-left:.75rem}.col{flex:1 0 0%}.col-1{flex:0 0 auto;width:8.3333333333%}.col-2{flex:0 0 auto;width:16.6666666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.3333333333%}.col-5{flex:0 0 auto;width:41.6666666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.3333333333%}.col-8{flex:0 0 auto;width:66.6666666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.3333333333%}.col-11{flex:0 0 auto;width:91.6666666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-0{margin-left:0}@media(min-width: 576px){.col-sm{flex:1 0 0%}.col-sm-1{flex:0 0 auto;width:8.3333333333%}.col-sm-2{flex:0 0 auto;width:16.6666666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.3333333333%}.col-sm-5{flex:0 0 auto;width:41.6666666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.3333333333%}.col-sm-8{flex:0 0 auto;width:66.6666666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.3333333333%}.col-sm-11{flex:0 0 auto;width:91.6666666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}}@media(min-width: 768px){.col-md{flex:1 0 0%}.col-md-1{flex:0 0 auto;width:8.3333333333%}.col-md-2{flex:0 0 auto;width:16.6666666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.3333333333%}.col-md-5{flex:0 0 auto;width:41.6666666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.3333333333%}.col-md-8{flex:0 0 auto;width:66.6666666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.3333333333%}.col-md-11{flex:0 0 auto;width:91.6666666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}}@media(min-width: 992px){.col-lg{flex:1 0 0%}.col-lg-1{flex:0 0 auto;width:8.3333333333%}.col-lg-2{flex:0 0 auto;width:16.6666666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.3333333333%}.col-lg-5{flex:0 0 auto;width:41.6666666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.3333333333%}.col-lg-8{flex:0 0 auto;width:66.6666666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.3333333333%}.col-lg-11{flex:0 0 auto;width:91.6666666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}}@media(min-width: 1200px){.col-xl{flex:1 0 0%}.col-xl-1{flex:0 0 auto;width:8.3333333333%}.col-xl-2{flex:0 0 auto;width:16.6666666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.3333333333%}.col-xl-5{flex:0 0 auto;width:41.6666666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.3333333333%}.col-xl-8{flex:0 0 auto;width:66.6666666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.3333333333%}.col-xl-11{flex:0 0 auto;width:91.6666666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.align-start{align-items:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;user-select:none;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;text-decoration:none}.btn:disabled,.btn.disabled{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.btn-primary:hover{background-color:#2b7bcf;border-color:#2a76c6}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{background-color:#596167;border-color:#545b62}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{background-color:#208637;border-color:#1e7e34}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{background-color:#c62232;border-color:#bd2130}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{background-color:#dda600;border-color:#d39e00}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{background-color:#128294;border-color:#117a8b}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{background-color:#e0e5e9;border-color:#dae0e5}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{background-color:#0f1112;border-color:#0a0c0d}.btn-outline-primary{color:#4a90d9;border-color:#4a90d9;background-color:rgba(0,0,0,0)}.btn-outline-primary:hover{color:#fff;background-color:#4a90d9}.btn-outline-secondary{color:#6c757d;border-color:#6c757d;background-color:rgba(0,0,0,0)}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d}.btn-outline-success{color:#28a745;border-color:#28a745;background-color:rgba(0,0,0,0)}.btn-outline-success:hover{color:#fff;background-color:#28a745}.btn-outline-danger{color:#dc3545;border-color:#dc3545;background-color:rgba(0,0,0,0)}.btn-outline-danger:hover{color:#fff;background-color:#dc3545}.btn-outline-warning{color:#ffc107;border-color:#ffc107;background-color:rgba(0,0,0,0)}.btn-outline-warning:hover{color:#212529;background-color:#ffc107}.btn-outline-info{color:#17a2b8;border-color:#17a2b8;background-color:rgba(0,0,0,0)}.btn-outline-info:hover{color:#fff;background-color:#17a2b8}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:rgba(0,0,0,0)}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa}.btn-outline-dark{color:#212529;border-color:#212529;background-color:rgba(0,0,0,0)}.btn-outline-dark:hover{color:#fff;background-color:#212529}.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.btn-lg{padding:.5rem 1rem;font-size:1.125rem;border-radius:.5rem}.btn-block{display:block;width:100%}.form-group{margin-bottom:1rem}.form-label{display:inline-block;margin-bottom:.5rem;font-weight:700}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}.form-control:focus{color:#212529;background-color:#fff;border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#f8f9fa;opacity:1}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}.form-select:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check-input{float:left;width:1em;height:1em;margin-left:-1.5em;margin-top:.25em;appearance:none;background-color:#fff;border:1px solid rgba(0,0,0,.25)}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:checked{background-color:#4a90d9;border-color:#4a90d9}.form-check-input:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-check-label{cursor:pointer}.is-valid{border-color:#28a745 !important}.is-valid:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.25) !important}.is-invalid{border-color:#dc3545 !important}.is-invalid:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.25) !important}.valid-feedback,.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em}.valid-feedback{color:#28a745}.invalid-feedback{color:#dc3545}.is-valid~.valid-feedback,.is-invalid~.invalid-feedback{display:block}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#f8f9fa;border:1px solid #ced4da;border-radius:.25rem}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.5rem}.card-header{padding:.75rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.5rem - 1px) calc(.5rem - 1px) 0 0}.card-body{flex:1 1 auto;padding:1rem}.card-footer{padding:.75rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.5rem - 1px) calc(.5rem - 1px)}.card-title{margin-bottom:.5rem;font-weight:700}.card-text:last-child{margin-bottom:0}.card-img-top{width:100%;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#4a90d9;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out}.nav-link:hover,.nav-link:focus{color:#256ab1}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-link.active{color:#4a90d9;font-weight:700}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;font-weight:700;color:inherit;text-decoration:none;white-space:nowrap}.navbar-brand:hover{color:inherit;opacity:.8}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggler{padding:.25rem .75rem;font-size:1.125rem;line-height:1;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,.1);border-radius:.25rem;cursor:pointer}.navbar-toggler:focus{outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center;display:none}.navbar-collapse.show{display:flex}@media(min-width: 576px){.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media(min-width: 768px){.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media(min-width: 992px){.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media(min-width: 1200px){.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media(min-width: 1400px){.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}@media(min-width: 992px){.navbar-nav{flex-direction:row}.navbar-collapse{display:flex;flex-basis:auto}.navbar-toggler{display:none}}.navbar-dark{color:#fff;background-color:#212529}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .nav-link{color:rgba(255,255,255,.75)}.navbar-dark .nav-link:hover,.navbar-dark .nav-link.active{color:#fff}.navbar-light{background-color:#f8f9fa}.navbar-light .nav-link{color:rgba(0,0,0,.55)}.navbar-light .nav-link:hover,.navbar-light .nav-link.active{color:rgba(0,0,0,.9)}.breadcrumb{display:flex;flex-wrap:wrap;padding:.5rem 1rem;margin-bottom:1rem;list-style:none;background-color:#f8f9fa;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#6c757d;content:"/"}.breadcrumb-item.active{color:#6c757d}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal.show{display:block}.modal-dialog{position:relative;width:auto;margin:1.75rem auto;max-width:500px}.modal-sm{max-width:300px}.modal-lg{max-width:800px}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.15);outline:0}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid rgba(0,0,0,.1)}.modal-title{margin-bottom:0;font-weight:700}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid rgba(0,0,0,.1);gap:.5rem}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:rgba(0,0,0,.5);display:none}.modal-backdrop.show{display:block}.alert{position:relative;padding:.75rem 1rem;margin-bottom:1rem;border:1px solid rgba(0,0,0,0);border-radius:.25rem}.alert-primary{color:#1c5187;background-color:#deeaf8;border-color:#b3d1ef}.alert-secondary{color:#313539;background-color:#caced1;border-color:#afb5ba}.alert-success{color:#0f401b;background-color:#9be7ac;border-color:#71dd8a}.alert-danger{color:#7c151f;background-color:#f6cdd1;border-color:#efa2a9}.alert-warning{color:#876500;background-color:#ffeeba;border-color:#ffe187}.alert-info{color:#093e47;background-color:#90e4f1;border-color:#63d9ec}.alert-light{color:#aeb9c5;background-color:#fff;border-color:#fff}.alert-dark{color:#000;background-color:#717e8c;border-color:#5a6570}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;padding:.9375rem 1rem;background:rgba(0,0,0,0);border:0;cursor:pointer;color:inherit;opacity:.5}.alert-dismissible .btn-close:hover{opacity:.75}.table{width:100%;margin-bottom:1rem;vertical-align:top;border-color:rgba(0,0,0,.1)}.table>:not(caption)>*>*{padding:.5rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:inherit}.table>thead{vertical-align:bottom;border-bottom:2px solid currentColor}.table>tbody>tr:last-child>*{border-bottom-color:rgba(0,0,0,0)}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered{border:1px solid rgba(0,0,0,.1)}.table-bordered>:not(caption)>*>*{border-width:1px;border-style:solid;border-color:inherit}.table-striped>tbody>tr:nth-of-type(odd)>*{background-color:rgba(0,0,0,.02)}.table-hover>tbody>tr:hover>*{background-color:rgba(0,0,0,.04)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.badge{display:inline-block;padding:.25em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:50rem}.badge-primary{color:#fff;background-color:#4a90d9}.badge-secondary{color:#fff;background-color:#6c757d}.badge-success{color:#fff;background-color:#28a745}.badge-danger{color:#fff;background-color:#dc3545}.badge-warning{color:#212529;background-color:#ffc107}.badge-info{color:#fff;background-color:#17a2b8}.badge-light{color:#212529;background-color:#f8f9fa}.badge-dark{color:#fff;background-color:#212529}.pagination{display:flex;flex-wrap:wrap;padding-left:0;list-style:none;gap:.25rem}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:rgba(0,0,0,.1)}.page-item.active .page-link{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.page-link{display:block;padding:.375rem .75rem;color:#4a90d9;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;text-decoration:none;line-height:1.25;transition:color .15s,background-color .15s,border-color .15s}.page-link:hover{color:#2a76c6;background-color:rgba(74,144,217,.08);border-color:rgba(0,0,0,.2)}.page-link:focus{outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.crud-img-preview{max-height:200px;max-width:100%;border-radius:.25rem;object-fit:cover}.crud-img-preview-sm{max-height:180px;max-width:100%;border-radius:.25rem;object-fit:cover}.d-none{display:none !important}.d-block{display:block !important}.d-flex{display:flex !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-grid{display:grid !important}@media(min-width: 576px){.d-sm-none{display:none !important}.d-sm-block{display:block !important}.d-sm-flex{display:flex !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}}@media(min-width: 768px){.d-md-none{display:none !important}.d-md-block{display:block !important}.d-md-flex{display:flex !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}}@media(min-width: 992px){.d-lg-none{display:none !important}.d-lg-block{display:block !important}.d-lg-flex{display:flex !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.mx-0{margin-left:0 !important;margin-right:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.mx-1{margin-left:.25rem !important;margin-right:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.mx-2{margin-left:.5rem !important;margin-right:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.mx-3{margin-left:1rem !important;margin-right:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.mx-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.mx-5{margin-left:3rem !important;margin-right:3rem !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.px-0{padding-left:0 !important;padding-right:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.px-1{padding-left:.25rem !important;padding-right:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.px-2{padding-left:.5rem !important;padding-right:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.px-3{padding-left:1rem !important;padding-right:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.px-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.px-5{padding-left:3rem !important;padding-right:3rem !important}.mx-auto{margin-left:auto !important;margin-right:auto !important}.text-start{text-align:left !important}.text-center{text-align:center !important}.text-end{text-align:right !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fw-bold{font-weight:700 !important}.fw-normal{font-weight:400 !important}.fw-light{font-weight:300 !important}.fs-1{font-size:2.25rem !important}.fs-2{font-size:1.875rem !important}.fs-3{font-size:1.5rem !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1rem !important}.fs-6{font-size:.875rem !important}.text-primary{color:#4a90d9 !important}.text-secondary{color:#6c757d !important}.text-success{color:#28a745 !important}.text-danger{color:#dc3545 !important}.text-warning{color:#ffc107 !important}.text-info{color:#17a2b8 !important}.text-light{color:#f8f9fa !important}.text-dark{color:#212529 !important}.text-muted{color:#6c757d !important}.text-white{color:#fff !important}.bg-primary{background-color:#4a90d9 !important}.bg-secondary{background-color:#6c757d !important}.bg-success{background-color:#28a745 !important}.bg-danger{background-color:#dc3545 !important}.bg-warning{background-color:#ffc107 !important}.bg-info{background-color:#17a2b8 !important}.bg-light{background-color:#f8f9fa !important}.bg-dark{background-color:#212529 !important}.bg-white{background-color:#fff !important}.border{border:1px solid rgba(0,0,0,.1) !important}.border-0{border:0 !important}.rounded{border-radius:.25rem !important}.rounded-0{border-radius:0 !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.w-25{width:25% !important}.h-25{height:25% !important}.w-50{width:50% !important}.h-50{height:50% !important}.w-75{width:75% !important}.h-75{height:75% !important}.w-100{width:100% !important}.h-100{height:100% !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.position-static{position:static !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.overflow-hidden{overflow:hidden !important}.overflow-auto{overflow:auto !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.collapse{display:none}.collapse.show{display:block}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.clearfix::after{display:block;clear:both;content:""}.cursor-pointer{cursor:pointer !important}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em;color:#000;background:rgba(0,0,0,0);border:0;opacity:.5;cursor:pointer;font-size:1.25rem;line-height:1}.btn-close:hover{opacity:.75}.btn-close:focus{opacity:1;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.1);border-radius:.25rem;max-width:100%;height:auto}.border-top{border-top:1px solid rgba(0,0,0,.1) !important}.border-bottom{border-bottom:1px solid rgba(0,0,0,.1) !important}.align-middle{vertical-align:middle !important}.align-top{vertical-align:top !important}.align-bottom{vertical-align:bottom !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}
|
|
1
|
+
*,*::before,*::after{box-sizing:border-box}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;scroll-behavior:smooth}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.25}p{margin-top:0;margin-bottom:1rem}ol,ul{padding-left:2rem;margin-top:0;margin-bottom:1rem}a{color:#4a90d9;text-decoration:none}a:hover{color:#256ab1;text-decoration:underline}img,svg{max-width:100%;height:auto;vertical-align:middle}table{border-collapse:collapse;caption-side:bottom}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;cursor:pointer}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid rgba(0,0,0,.1);opacity:.25}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}[hidden]{display:none !important}h1{font-size:2.25rem}h2{font-size:1.875rem}h3{font-size:1.5rem}h4{font-size:1.25rem}h5{font-size:1.125rem}h6{font-size:1rem}small,.small{font-size:.875em}mark,.mark{padding:.2em;background-color:rgba(255,193,7,.3)}blockquote{margin:0 0 1rem;padding:.5rem 1rem;border-left:.25rem solid rgba(0,0,0,.1);font-size:1.125rem}code{font-size:.875em;color:#dc3545;word-wrap:break-word}pre{display:block;padding:1rem;font-size:.875em;color:#212529;background-color:#f8f9fa;border-radius:.25rem}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.container,.container-fluid{width:100%;padding-right:.75rem;padding-left:.75rem;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}@media (min-width: 1400px){.container{max-width:1320px}}.row{display:flex;flex-wrap:wrap;margin-right:-.75rem;margin-left:-.75rem}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:.75rem;padding-left:.75rem}.col{flex:1 0 0%}.col-1{flex:0 0 auto;width:8.3333333333%}.col-2{flex:0 0 auto;width:16.6666666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.3333333333%}.col-5{flex:0 0 auto;width:41.6666666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.3333333333%}.col-8{flex:0 0 auto;width:66.6666666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.3333333333%}.col-11{flex:0 0 auto;width:91.6666666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-0{margin-left:0}@media (min-width: 576px){.col-sm{flex:1 0 0%}.col-sm-1{flex:0 0 auto;width:8.3333333333%}.col-sm-2{flex:0 0 auto;width:16.6666666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.3333333333%}.col-sm-5{flex:0 0 auto;width:41.6666666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.3333333333%}.col-sm-8{flex:0 0 auto;width:66.6666666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.3333333333%}.col-sm-11{flex:0 0 auto;width:91.6666666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}}@media (min-width: 768px){.col-md{flex:1 0 0%}.col-md-1{flex:0 0 auto;width:8.3333333333%}.col-md-2{flex:0 0 auto;width:16.6666666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.3333333333%}.col-md-5{flex:0 0 auto;width:41.6666666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.3333333333%}.col-md-8{flex:0 0 auto;width:66.6666666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.3333333333%}.col-md-11{flex:0 0 auto;width:91.6666666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}}@media (min-width: 992px){.col-lg{flex:1 0 0%}.col-lg-1{flex:0 0 auto;width:8.3333333333%}.col-lg-2{flex:0 0 auto;width:16.6666666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.3333333333%}.col-lg-5{flex:0 0 auto;width:41.6666666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.3333333333%}.col-lg-8{flex:0 0 auto;width:66.6666666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.3333333333%}.col-lg-11{flex:0 0 auto;width:91.6666666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}}@media (min-width: 1200px){.col-xl{flex:1 0 0%}.col-xl-1{flex:0 0 auto;width:8.3333333333%}.col-xl-2{flex:0 0 auto;width:16.6666666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.3333333333%}.col-xl-5{flex:0 0 auto;width:41.6666666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.3333333333%}.col-xl-8{flex:0 0 auto;width:66.6666666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.3333333333%}.col-xl-11{flex:0 0 auto;width:91.6666666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.align-start{align-items:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;user-select:none;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;text-decoration:none}.btn:disabled,.btn.disabled{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.btn-primary:hover{background-color:#2b7bcf;border-color:#2a76c6}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{background-color:#596167;border-color:#545b62}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{background-color:#208637;border-color:#1e7e34}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{background-color:#c62232;border-color:#bd2130}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{background-color:#dda600;border-color:#d39e00}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{background-color:#128294;border-color:#117a8b}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{background-color:#e0e5e9;border-color:#dae0e5}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{background-color:#0f1112;border-color:#0a0c0d}.btn-outline-primary{color:#4a90d9;border-color:#4a90d9;background-color:rgba(0,0,0,0)}.btn-outline-primary:hover{color:#fff;background-color:#4a90d9}.btn-outline-secondary{color:#6c757d;border-color:#6c757d;background-color:rgba(0,0,0,0)}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d}.btn-outline-success{color:#28a745;border-color:#28a745;background-color:rgba(0,0,0,0)}.btn-outline-success:hover{color:#fff;background-color:#28a745}.btn-outline-danger{color:#dc3545;border-color:#dc3545;background-color:rgba(0,0,0,0)}.btn-outline-danger:hover{color:#fff;background-color:#dc3545}.btn-outline-warning{color:#ffc107;border-color:#ffc107;background-color:rgba(0,0,0,0)}.btn-outline-warning:hover{color:#212529;background-color:#ffc107}.btn-outline-info{color:#17a2b8;border-color:#17a2b8;background-color:rgba(0,0,0,0)}.btn-outline-info:hover{color:#fff;background-color:#17a2b8}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:rgba(0,0,0,0)}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa}.btn-outline-dark{color:#212529;border-color:#212529;background-color:rgba(0,0,0,0)}.btn-outline-dark:hover{color:#fff;background-color:#212529}.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.btn-lg{padding:.5rem 1rem;font-size:1.125rem;border-radius:.5rem}.btn-block{display:block;width:100%}.form-group{margin-bottom:1rem}.form-label{display:inline-block;margin-bottom:.5rem;font-weight:700}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}.form-control:focus{color:#212529;background-color:#fff;border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#f8f9fa;opacity:1}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}.form-select:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check-input{float:left;width:1em;height:1em;margin-left:-1.5em;margin-top:.25em;appearance:none;background-color:#fff;border:1px solid rgba(0,0,0,.25)}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:checked{background-color:#4a90d9;border-color:#4a90d9}.form-check-input:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-check-label{cursor:pointer}.is-valid{border-color:#28a745 !important}.is-valid:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.25) !important}.is-invalid{border-color:#dc3545 !important}.is-invalid:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.25) !important}.valid-feedback,.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em}.valid-feedback{color:#28a745}.invalid-feedback{color:#dc3545}.is-valid~.valid-feedback,.is-invalid~.invalid-feedback{display:block}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#f8f9fa;border:1px solid #ced4da;border-radius:.25rem}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.5rem}.card-header{padding:.75rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.5rem - 1px) calc(.5rem - 1px) 0 0}.card-body{flex:1 1 auto;padding:1rem}.card-footer{padding:.75rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.5rem - 1px) calc(.5rem - 1px)}.card-title{margin-bottom:.5rem;font-weight:700}.card-text:last-child{margin-bottom:0}.card-img-top{width:100%;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#4a90d9;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out}.nav-link:hover,.nav-link:focus{color:#256ab1}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-link.active{color:#4a90d9;font-weight:700}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;font-weight:700;color:inherit;text-decoration:none;white-space:nowrap}.navbar-brand:hover{color:inherit;opacity:.8}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggler{padding:.25rem .75rem;font-size:1.125rem;line-height:1;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,.1);border-radius:.25rem;cursor:pointer}.navbar-toggler:focus{outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center;display:none}.navbar-collapse.show{display:flex}@media (min-width: 576px){.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width: 768px){.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width: 992px){.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width: 1200px){.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width: 1400px){.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}@media (min-width: 992px){.navbar-nav{flex-direction:row}.navbar-collapse{display:flex;flex-basis:auto}.navbar-toggler{display:none}}.navbar-dark{color:#fff;background-color:#212529}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .nav-link{color:rgba(255,255,255,.75)}.navbar-dark .nav-link:hover,.navbar-dark .nav-link.active{color:#fff}.navbar-light{background-color:#f8f9fa}.navbar-light .nav-link{color:rgba(0,0,0,.55)}.navbar-light .nav-link:hover,.navbar-light .nav-link.active{color:rgba(0,0,0,.9)}.breadcrumb{display:flex;flex-wrap:wrap;padding:.5rem 1rem;margin-bottom:1rem;list-style:none;background-color:#f8f9fa;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#6c757d;content:"/"}.breadcrumb-item.active{color:#6c757d}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal.show{display:block}.modal-dialog{position:relative;width:auto;margin:1.75rem auto;max-width:500px}.modal-sm{max-width:300px}.modal-lg{max-width:800px}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.15);outline:0}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid rgba(0,0,0,.1)}.modal-title{margin-bottom:0;font-weight:700}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid rgba(0,0,0,.1);gap:.5rem}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:rgba(0,0,0,.5);display:none}.modal-backdrop.show{display:block}.alert{position:relative;padding:.75rem 1rem;margin-bottom:1rem;border:1px solid rgba(0,0,0,0);border-radius:.25rem}.alert-primary{color:#1c5187;background-color:#deeaf8;border-color:#b3d1ef}.alert-secondary{color:#313539;background-color:#caced1;border-color:#afb5ba}.alert-success{color:#0f401b;background-color:#9be7ac;border-color:#71dd8a}.alert-danger{color:#7c151f;background-color:#f6cdd1;border-color:#efa2a9}.alert-warning{color:#876500;background-color:#ffeeba;border-color:#ffe187}.alert-info{color:#093e47;background-color:#90e4f1;border-color:#63d9ec}.alert-light{color:#aeb9c5;background-color:#fff;border-color:#fff}.alert-dark{color:#000;background-color:#717e8c;border-color:#5a6570}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;padding:.9375rem 1rem;background:rgba(0,0,0,0);border:0;cursor:pointer;color:inherit;opacity:.5}.alert-dismissible .btn-close:hover{opacity:.75}.table{width:100%;margin-bottom:1rem;vertical-align:top;border-color:rgba(0,0,0,.1)}.table>:not(caption)>*>*{padding:.5rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:inherit}.table>thead{vertical-align:bottom;border-bottom:2px solid currentColor}.table>tbody>tr:last-child>*{border-bottom-color:rgba(0,0,0,0)}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered{border:1px solid rgba(0,0,0,.1)}.table-bordered>:not(caption)>*>*{border-width:1px;border-style:solid;border-color:inherit}.table-striped>tbody>tr:nth-of-type(odd)>*{background-color:rgba(0,0,0,.02)}.table-hover>tbody>tr:hover>*{background-color:rgba(0,0,0,.04)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.badge{display:inline-block;padding:.25em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:50rem}.badge-primary{color:#fff;background-color:#4a90d9}.badge-secondary{color:#fff;background-color:#6c757d}.badge-success{color:#fff;background-color:#28a745}.badge-danger{color:#fff;background-color:#dc3545}.badge-warning{color:#212529;background-color:#ffc107}.badge-info{color:#fff;background-color:#17a2b8}.badge-light{color:#212529;background-color:#f8f9fa}.badge-dark{color:#fff;background-color:#212529}.pagination{display:flex;flex-wrap:wrap;padding-left:0;list-style:none;gap:.25rem}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:rgba(0,0,0,.1)}.page-item.active .page-link{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.page-link{display:block;padding:.375rem .75rem;color:#4a90d9;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;text-decoration:none;line-height:1.25;transition:color .15s,background-color .15s,border-color .15s}.page-link:hover{color:#2a76c6;background-color:rgba(74,144,217,.08);border-color:rgba(0,0,0,.2)}.page-link:focus{outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.crud-img-preview{max-height:200px;max-width:100%;border-radius:.25rem;object-fit:cover}.crud-img-preview-sm{max-height:180px;max-width:100%;border-radius:.25rem;object-fit:cover}.d-none{display:none !important}.d-block{display:block !important}.d-flex{display:flex !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-grid{display:grid !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-block{display:block !important}.d-sm-flex{display:flex !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-block{display:block !important}.d-md-flex{display:flex !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-block{display:block !important}.d-lg-flex{display:flex !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.mx-0{margin-left:0 !important;margin-right:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.mx-1{margin-left:.25rem !important;margin-right:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.mx-2{margin-left:.5rem !important;margin-right:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.mx-3{margin-left:1rem !important;margin-right:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.mx-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.mx-5{margin-left:3rem !important;margin-right:3rem !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.px-0{padding-left:0 !important;padding-right:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.px-1{padding-left:.25rem !important;padding-right:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.px-2{padding-left:.5rem !important;padding-right:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.px-3{padding-left:1rem !important;padding-right:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.px-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.px-5{padding-left:3rem !important;padding-right:3rem !important}.mx-auto{margin-left:auto !important;margin-right:auto !important}.text-start{text-align:left !important}.text-center{text-align:center !important}.text-end{text-align:right !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fw-bold{font-weight:700 !important}.fw-normal{font-weight:400 !important}.fw-light{font-weight:300 !important}.fs-1{font-size:2.25rem !important}.fs-2{font-size:1.875rem !important}.fs-3{font-size:1.5rem !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1rem !important}.fs-6{font-size:.875rem !important}.text-primary{color:#4a90d9 !important}.text-secondary{color:#6c757d !important}.text-success{color:#28a745 !important}.text-danger{color:#dc3545 !important}.text-warning{color:#ffc107 !important}.text-info{color:#17a2b8 !important}.text-light{color:#f8f9fa !important}.text-dark{color:#212529 !important}.text-muted{color:#6c757d !important}.text-white{color:#fff !important}.bg-primary{background-color:#4a90d9 !important}.bg-secondary{background-color:#6c757d !important}.bg-success{background-color:#28a745 !important}.bg-danger{background-color:#dc3545 !important}.bg-warning{background-color:#ffc107 !important}.bg-info{background-color:#17a2b8 !important}.bg-light{background-color:#f8f9fa !important}.bg-dark{background-color:#212529 !important}.bg-white{background-color:#fff !important}.border{border:1px solid rgba(0,0,0,.1) !important}.border-0{border:0 !important}.rounded{border-radius:.25rem !important}.rounded-0{border-radius:0 !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.w-25{width:25% !important}.h-25{height:25% !important}.w-50{width:50% !important}.h-50{height:50% !important}.w-75{width:75% !important}.h-75{height:75% !important}.w-100{width:100% !important}.h-100{height:100% !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.position-static{position:static !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.overflow-hidden{overflow:hidden !important}.overflow-auto{overflow:auto !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.collapse{display:none}.collapse.show{display:block}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.clearfix::after{display:block;clear:both;content:""}.cursor-pointer{cursor:pointer !important}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em;color:#000;background:rgba(0,0,0,0);border:0;opacity:.5;cursor:pointer;font-size:1.25rem;line-height:1}.btn-close:hover{opacity:.75}.btn-close:focus{opacity:1;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.1);border-radius:.25rem;max-width:100%;height:auto}.border-top{border-top:1px solid rgba(0,0,0,.1) !important}.border-bottom{border-bottom:1px solid rgba(0,0,0,.1) !important}.align-middle{vertical-align:middle !important}.align-top{vertical-align:top !important}.align-bottom{vertical-align:bottom !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}
|
data/lib/tina4/queue.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "json"
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require "uri"
|
|
4
5
|
require_relative "job"
|
|
5
6
|
|
|
6
7
|
module Tina4
|
|
@@ -35,6 +36,67 @@ module Tina4
|
|
|
35
36
|
@backend = resolve_backend_arg(backend)
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# ── The file store's layout: the ONE answer to "where do the queue files
|
|
40
|
+
# live". ─────────────────────────────────────────────────────────────────
|
|
41
|
+
#
|
|
42
|
+
# Everything that touches the store asks here — the lite backend that
|
|
43
|
+
# writes it, and every dev-admin queue handler that reads it. It exists
|
|
44
|
+
# because they used to disagree: the dev admin scanned a hardcoded
|
|
45
|
+
# Dir.pwd/data/queue while the backend wrote to Dir.pwd/.queue and read
|
|
46
|
+
# TINA4_QUEUE_PATH nowhere, so the panel counted one directory and listed
|
|
47
|
+
# another, and its topic list could never name a real topic. Re-deriving
|
|
48
|
+
# any of this anywhere else re-opens that defect.
|
|
49
|
+
#
|
|
50
|
+
# The layout is the canonical cross-framework one, identical to the Python
|
|
51
|
+
# master, PHP and Node:
|
|
52
|
+
#
|
|
53
|
+
# <base>/<topic>/*.queue-data pending
|
|
54
|
+
# <base>/<topic>/reserved/*.queue-data reserved (in flight)
|
|
55
|
+
# <base>/dead_letter/*.queue-data dead-lettered, tagged by topic
|
|
56
|
+
|
|
57
|
+
# Canonical job-file extension in all four frameworks.
|
|
58
|
+
JOB_EXTENSION = ".queue-data"
|
|
59
|
+
|
|
60
|
+
# Dead letters share one directory and carry their topic in the record.
|
|
61
|
+
DEAD_LETTER_DIRNAME = "dead_letter"
|
|
62
|
+
|
|
63
|
+
# Root of the file store: TINA4_QUEUE_PATH, else "data/queue" relative to
|
|
64
|
+
# the working directory.
|
|
65
|
+
#
|
|
66
|
+
# Absolute, so every caller gets the same directory rather than one that
|
|
67
|
+
# depends on when it was resolved. A blank value is treated as unset (the
|
|
68
|
+
# same normalisation TINA4_SESSION_BACKEND applies) — File.join("", topic)
|
|
69
|
+
# would otherwise silently address /<topic>, at the filesystem root.
|
|
70
|
+
def self.base_path
|
|
71
|
+
configured = ENV["TINA4_QUEUE_PATH"].to_s.strip
|
|
72
|
+
File.expand_path(configured.empty? ? "data/queue" : configured)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The single path segment a topic maps to.
|
|
76
|
+
#
|
|
77
|
+
# Sanitised HERE, in the one place that resolves it, so no caller can build
|
|
78
|
+
# a store path out of an unsanitised topic name — the dev-admin queue panel
|
|
79
|
+
# takes its topic straight off a query string, where "../.." would
|
|
80
|
+
# otherwise walk out of the store.
|
|
81
|
+
def self.topic_dirname(topic)
|
|
82
|
+
topic.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Directory holding +topic+'s pending jobs.
|
|
86
|
+
def self.topic_path(topic)
|
|
87
|
+
File.join(base_path, topic_dirname(topic))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Every job file in +dir+, name-sorted so two readers of the same directory
|
|
91
|
+
# always see the same order.
|
|
92
|
+
def self.job_files(dir)
|
|
93
|
+
Dir.glob(File.join(dir, "*#{JOB_EXTENSION}")).sort
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.job_file(dir, id)
|
|
97
|
+
File.join(dir, "#{id}#{JOB_EXTENSION}")
|
|
98
|
+
end
|
|
99
|
+
|
|
38
100
|
# Reservation/visibility timeout in seconds, from env (default 300 = 5 min).
|
|
39
101
|
def self.default_visibility_timeout
|
|
40
102
|
Float(ENV.fetch("TINA4_QUEUE_VISIBILITY_TIMEOUT", "300"))
|
|
@@ -77,43 +139,89 @@ module Tina4
|
|
|
77
139
|
|
|
78
140
|
# Clear all pending jobs from this queue's topic. Returns count removed.
|
|
79
141
|
def clear # -> int
|
|
80
|
-
return 0
|
|
142
|
+
# Was `return 0` - a SILENT no-op that looked exactly like an empty
|
|
143
|
+
# queue. A backend that cannot clear says so, naming itself.
|
|
144
|
+
unless @backend.respond_to?(:clear)
|
|
145
|
+
raise NotImplementedError,
|
|
146
|
+
"The #{@backend.class.name.split('::').last} queue backend cannot " \
|
|
147
|
+
"perform clear(): it cannot remove jobs by topic. Use the file or " \
|
|
148
|
+
"mongodb backend."
|
|
149
|
+
end
|
|
81
150
|
@backend.clear(@topic)
|
|
82
151
|
end
|
|
83
152
|
|
|
153
|
+
# Release the backend's connection and free its resources.
|
|
154
|
+
#
|
|
155
|
+
# A queue on RabbitMQ, Kafka or MongoDB holds a REAL client and socket.
|
|
156
|
+
# Until 3.13.95 there was no way to hand it back: close existed on the
|
|
157
|
+
# rabbitmq/mongo/kafka backends but was surfaced on NOTHING, and the lite
|
|
158
|
+
# backend had none at all - so every `backend.close if respond_to?(:close)`
|
|
159
|
+
# guard in the tree silently skipped the default backend. An app that built
|
|
160
|
+
# a Queue per request leaked one client per request, invisibly, until the
|
|
161
|
+
# broker refused new connections. Same class of leak as ADR-0025 corollary 4
|
|
162
|
+
# (client-lifecycle-is-bounded).
|
|
163
|
+
#
|
|
164
|
+
# Safe on EVERY backend: the lite backend holds no connection and closes as
|
|
165
|
+
# a documented no-op, so a TINA4_QUEUE_BACKEND change never turns a working
|
|
166
|
+
# shutdown path into an error. Idempotent - each backend drops its handles
|
|
167
|
+
# on the first call, so a second call finds nothing to close and returns.
|
|
168
|
+
#
|
|
169
|
+
# Treat the queue as spent afterwards and build a new one to keep working.
|
|
170
|
+
def close
|
|
171
|
+
@backend.close
|
|
172
|
+
end
|
|
173
|
+
|
|
84
174
|
# Get jobs that failed but are still eligible for retry (under max_retries).
|
|
85
175
|
def failed # -> list[dict]
|
|
86
|
-
|
|
176
|
+
refuse_operation!("failed()") unless @backend.respond_to?(:failed)
|
|
87
177
|
@backend.failed(@topic, max_retries: @max_retries)
|
|
88
178
|
end
|
|
89
179
|
|
|
90
180
|
# Retry a specific failed job by ID, or all dead-letter jobs if no id given.
|
|
91
181
|
# Returns true if re-queued.
|
|
92
182
|
def retry(job_id = nil, delay_seconds: 0) # -> bool
|
|
93
|
-
|
|
183
|
+
refuse_operation!("retry()") unless @backend.respond_to?(:retry_job)
|
|
94
184
|
@backend.retry_job(@topic, job_id: job_id, delay_seconds: delay_seconds)
|
|
95
185
|
end
|
|
96
186
|
|
|
97
187
|
# Get dead letter jobs — messages that exceeded max retries.
|
|
98
188
|
# Pass max_retries to override the queue's default.
|
|
99
189
|
def dead_letters(max_retries: nil) # -> list[dict]
|
|
100
|
-
|
|
190
|
+
refuse_operation!("dead_letters()") unless @backend.respond_to?(:dead_letters)
|
|
101
191
|
@backend.dead_letters(@topic, max_retries: max_retries || @max_retries)
|
|
102
192
|
end
|
|
103
193
|
|
|
104
194
|
# Delete messages by status (completed, failed, dead).
|
|
105
195
|
def purge(status, max_retries: nil) # -> int
|
|
106
|
-
|
|
196
|
+
refuse_operation!("purge()") unless @backend.respond_to?(:purge)
|
|
107
197
|
@backend.purge(@topic, status)
|
|
108
198
|
end
|
|
109
199
|
|
|
110
200
|
# Re-queue failed messages (under max_retries) back to pending.
|
|
111
201
|
# Returns the number of jobs re-queued.
|
|
112
202
|
def retry_failed(max_retries: nil) # -> int
|
|
113
|
-
|
|
203
|
+
refuse_operation!("retry_failed()") unless @backend.respond_to?(:retry_failed)
|
|
114
204
|
@backend.retry_failed(@topic, max_retries: max_retries || @max_retries)
|
|
115
205
|
end
|
|
116
206
|
|
|
207
|
+
# A backend that does not implement an operation must SAY SO, naming itself
|
|
208
|
+
# and the operation (invariant 6).
|
|
209
|
+
#
|
|
210
|
+
# These five methods used to `return [] / false / 0 unless
|
|
211
|
+
# @backend.respond_to?(...)`. That turned "this backend cannot do this" into
|
|
212
|
+
# "nothing has failed" / "nothing needed retrying" / "nothing was purged" --
|
|
213
|
+
# answers indistinguishable from a genuine empty success (ADR-0022
|
|
214
|
+
# decision 7). It is the silent-no-op class this invariant exists to remove,
|
|
215
|
+
# and it hid every missing broker method behind a plausible-looking result.
|
|
216
|
+
def refuse_operation!(operation)
|
|
217
|
+
name = @backend.class.name.to_s.split("::").last.sub(/Backend\z/, "").downcase
|
|
218
|
+
raise NotImplementedError,
|
|
219
|
+
"The #{name} queue backend cannot perform #{operation}: it does not " \
|
|
220
|
+
"implement that operation. Returning an empty result would be " \
|
|
221
|
+
"indistinguishable from a successful empty answer. Use the file or " \
|
|
222
|
+
"mongodb backend, which implement the full failure lifecycle."
|
|
223
|
+
end
|
|
224
|
+
|
|
117
225
|
# Produce a message onto a topic. Convenience wrapper around push().
|
|
118
226
|
def produce(topic, payload, priority: 0, delay_seconds: 0)
|
|
119
227
|
available_at = delay_seconds > 0 ? Time.now + delay_seconds : nil
|
|
@@ -210,7 +318,13 @@ module Tina4
|
|
|
210
318
|
# the given topic (defaults to this queue's topic). Returns the matching
|
|
211
319
|
# Job (claimed/removed from the queue) or nil.
|
|
212
320
|
def pop_by_id(topic = nil, id)
|
|
213
|
-
return nil
|
|
321
|
+
# Was `return nil` - indistinguishable from "no such job".
|
|
322
|
+
unless @backend.respond_to?(:find_by_id)
|
|
323
|
+
raise NotImplementedError,
|
|
324
|
+
"The #{@backend.class.name.split('::').last} queue backend cannot " \
|
|
325
|
+
"perform pop_by_id(): it cannot address a single message by id. Use " \
|
|
326
|
+
"the file or mongodb backend."
|
|
327
|
+
end
|
|
214
328
|
attach(@backend.find_by_id(topic || @topic, id))
|
|
215
329
|
end
|
|
216
330
|
|
|
@@ -290,7 +404,12 @@ module Tina4
|
|
|
290
404
|
|
|
291
405
|
# Resolve the default backend from env vars.
|
|
292
406
|
def self.resolve_backend(name = nil, max_retries: 3, retry_backoff: 0, visibility_timeout: nil)
|
|
293
|
-
|
|
407
|
+
# Normalise BOTH sources, not just the env var. `.downcase.strip` used to
|
|
408
|
+
# bind only to the ENV.fetch branch, so an explicit
|
|
409
|
+
# Queue.new(backend: "FILE") fell through to the unknown-backend raise
|
|
410
|
+
# while the identical spelling in TINA4_QUEUE_BACKEND resolved fine -
|
|
411
|
+
# and while python, php and nodejs all accepted it. Python is master here.
|
|
412
|
+
chosen = (name || ENV.fetch("TINA4_QUEUE_BACKEND", "file")).to_s.downcase.strip
|
|
294
413
|
vt = visibility_timeout.nil? ? default_visibility_timeout : visibility_timeout
|
|
295
414
|
|
|
296
415
|
case chosen.to_s
|
|
@@ -301,11 +420,19 @@ module Tina4
|
|
|
301
420
|
when "rabbitmq"
|
|
302
421
|
# Broker manages visibility/redelivery (unacked messages requeue on
|
|
303
422
|
# channel close) — the framework timeout is accepted but not used.
|
|
423
|
+
# max_retries IS used: fail() counts attempts itself and dead-letters
|
|
424
|
+
# past the limit. Without threading it through, the backend fell back to
|
|
425
|
+
# its own default of 3, so Queue.new(max_retries: 2) gave a job THREE
|
|
426
|
+
# attempts on rabbitmq and two on file — the dead-letter threshold
|
|
427
|
+
# silently changed with the provider.
|
|
304
428
|
config = resolve_rabbitmq_config
|
|
429
|
+
config[:max_retries] = max_retries
|
|
305
430
|
Tina4::QueueBackends::RabbitmqBackend.new(config)
|
|
306
431
|
when "kafka"
|
|
307
432
|
# Consumer-group offsets manage redelivery — framework timeout N/A.
|
|
433
|
+
# max_retries threaded through for the same reason as rabbitmq.
|
|
308
434
|
config = resolve_kafka_config
|
|
435
|
+
config[:max_retries] = max_retries
|
|
309
436
|
Tina4::QueueBackends::KafkaBackend.new(config)
|
|
310
437
|
when "mongodb", "mongo"
|
|
311
438
|
config = resolve_mongo_config
|
|
@@ -403,7 +530,25 @@ module Tina4
|
|
|
403
530
|
|
|
404
531
|
if rest.include?("/")
|
|
405
532
|
hostport, vhost = rest.split("/", 2)
|
|
406
|
-
|
|
533
|
+
# THE VHOST IS THE PATH SEGMENT, URL-DECODED, WITH NO LEADING SLASH
|
|
534
|
+
# (RabbitMQ URI spec). This used to prepend "/", so
|
|
535
|
+
# amqp://guest:guest@rabbit:5672/orders asked for a vhost literally
|
|
536
|
+
# named "/orders". No broker has that one - it is named "orders" - so
|
|
537
|
+
# every publish failed against a named vhost, which is the ordinary
|
|
538
|
+
# multi-tenant setup and the form every RabbitMQ tutorial shows.
|
|
539
|
+
# MEASURED against a real broker: 4 of 5 URL shapes resolved to the
|
|
540
|
+
# wrong name, and the only one that worked carried no vhost at all,
|
|
541
|
+
# which is why four green suites never noticed.
|
|
542
|
+
#
|
|
543
|
+
# Decoding matters for the same reason: the DEFAULT vhost is named "/",
|
|
544
|
+
# which cannot appear literally in a path, so the spec spells it "%2f".
|
|
545
|
+
#
|
|
546
|
+
# DELIBERATE DEVIATION, one shape: the spec reads a bare trailing slash
|
|
547
|
+
# as the EMPTY vhost name. Tina4 treats it as "not specified" and keeps
|
|
548
|
+
# the caller's default - nobody writes a trailing slash intending a
|
|
549
|
+
# vhost named "", and reading it literally would break a working
|
|
550
|
+
# "amqp://host:5672/" for no benefit.
|
|
551
|
+
config[:vhost] = URI::DEFAULT_PARSER.unescape(vhost) if vhost && !vhost.empty?
|
|
407
552
|
else
|
|
408
553
|
hostport = rest
|
|
409
554
|
end
|
|
@@ -7,8 +7,13 @@ module Tina4
|
|
|
7
7
|
require "rdkafka"
|
|
8
8
|
@brokers = options[:brokers] || "localhost:9092"
|
|
9
9
|
@group_id = options[:group_id] || "tina4_consumer_group"
|
|
10
|
+
@max_retries = options[:max_retries] || 3
|
|
10
11
|
|
|
11
12
|
security = self.class._security_config
|
|
13
|
+
# Kept so dead_letters() can build a short-lived READER consumer with
|
|
14
|
+
# its own group id, which reads the dead-letter topic from the start
|
|
15
|
+
# without disturbing the main subscription or committing offsets.
|
|
16
|
+
@security = security
|
|
12
17
|
|
|
13
18
|
producer_config = {
|
|
14
19
|
"bootstrap.servers" => @brokers
|
|
@@ -35,6 +40,9 @@ module Tina4
|
|
|
35
40
|
# many Kafka deployments already set. Honours security.protocol (e.g. SSL,
|
|
36
41
|
# SASL_SSL), ssl.ca.location, and optional SASL (mechanism / username /
|
|
37
42
|
# password). Unset keys are omitted, leaving librdkafka's PLAINTEXT default.
|
|
43
|
+
# Queue propagates its own configuration onto the backend after construction.
|
|
44
|
+
attr_accessor :max_retries
|
|
45
|
+
|
|
38
46
|
def self._security_config
|
|
39
47
|
# rdkafka key -> env suffix (read as TINA4_KAFKA_<suffix>, then KAFKA_<suffix>)
|
|
40
48
|
mapping = {
|
|
@@ -63,6 +71,22 @@ module Tina4
|
|
|
63
71
|
private_class_method :env_value
|
|
64
72
|
|
|
65
73
|
def enqueue(message)
|
|
74
|
+
if message.priority.to_i > 0
|
|
75
|
+
raise NotImplementedError,
|
|
76
|
+
"The kafka queue backend cannot honour push(priority): Kafka has no " \
|
|
77
|
+
"priority concept at all - a consumer reads a partition in offset " \
|
|
78
|
+
"order. Use the file or mongodb backend for prioritised jobs."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if message.available_at
|
|
82
|
+
raise NotImplementedError,
|
|
83
|
+
"The kafka queue backend cannot honour push(delay_seconds): Kafka " \
|
|
84
|
+
"has no per-message delay at all. A consumer reads a partition in " \
|
|
85
|
+
"offset order, so a delayed record would stall every record behind " \
|
|
86
|
+
"it. Use the file or mongodb backend for delayed jobs, or schedule " \
|
|
87
|
+
"the push itself."
|
|
88
|
+
end
|
|
89
|
+
|
|
66
90
|
@producer.produce(
|
|
67
91
|
topic: message.topic,
|
|
68
92
|
payload: message.to_json,
|
|
@@ -99,15 +123,31 @@ module Tina4
|
|
|
99
123
|
data = JSON.parse(msg.payload)
|
|
100
124
|
@last_message = msg
|
|
101
125
|
|
|
126
|
+
# attempts and error MUST be carried back. Rebuilding from
|
|
127
|
+
# topic/payload/id alone reset attempts to 0 on every redelivery, so
|
|
128
|
+
# fail()'s attempts >= max_retries check could never trip and a poison
|
|
129
|
+
# record would be re-produced forever instead of dead-lettering.
|
|
102
130
|
Tina4::Job.new(
|
|
103
131
|
topic: data["topic"],
|
|
104
132
|
payload: data["payload"],
|
|
105
|
-
id: data["id"]
|
|
133
|
+
id: data["id"],
|
|
134
|
+
attempts: data["attempts"] || 0,
|
|
135
|
+
error: data["error"]
|
|
106
136
|
)
|
|
107
137
|
rescue Rdkafka::RdkafkaError
|
|
108
138
|
nil
|
|
109
139
|
end
|
|
110
140
|
|
|
141
|
+
# Kafka has no queue depth: a log is a sequence of offsets, not a queue,
|
|
142
|
+
# and computing a "remaining" count means an admin round-trip per call.
|
|
143
|
+
# ADR-0022 decision 5 records 0 as the documented answer, which Python and
|
|
144
|
+
# PHP already return. Ruby had no size method at all, so queue.size raised
|
|
145
|
+
# NoMethodError on kafka - a method that does not resolve at all, which is
|
|
146
|
+
# what invariant 1 forbids.
|
|
147
|
+
def size(_topic)
|
|
148
|
+
0
|
|
149
|
+
end
|
|
150
|
+
|
|
111
151
|
def acknowledge(_message)
|
|
112
152
|
@consumer.commit if @last_message
|
|
113
153
|
end
|
|
@@ -117,17 +157,166 @@ module Tina4
|
|
|
117
157
|
end
|
|
118
158
|
|
|
119
159
|
def dead_letter(message)
|
|
160
|
+
# attempts and error MUST ride along, else every Kafka dead letter reads
|
|
161
|
+
# back as attempts=0 with no reason while file/mongodb report the real
|
|
162
|
+
# values.
|
|
120
163
|
dead_msg = Tina4::Job.new(
|
|
121
164
|
topic: "#{message.topic}.dead_letter",
|
|
122
165
|
payload: message.payload,
|
|
123
|
-
id: message.id
|
|
166
|
+
id: message.id,
|
|
167
|
+
attempts: message.attempts,
|
|
168
|
+
error: message.error
|
|
124
169
|
)
|
|
125
170
|
enqueue(dead_msg)
|
|
126
171
|
end
|
|
127
172
|
|
|
173
|
+
# Terminal ack. Job#complete calls backend.complete, and this backend only
|
|
174
|
+
# had acknowledge() - so job.complete was a silent no-op on kafka and the
|
|
175
|
+
# consumer-group offset was never committed, making the broker redeliver
|
|
176
|
+
# every completed record.
|
|
177
|
+
def complete(message)
|
|
178
|
+
acknowledge(message)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Record a failed attempt, then retry it or dead-letter it.
|
|
182
|
+
#
|
|
183
|
+
# This did not exist. Job#fail guarded on respond_to?(:fail) and silently
|
|
184
|
+
# degraded to in-memory bookkeeping, so job.fail() NEVER reached Kafka:
|
|
185
|
+
# the offset was never committed and no dead letter was produced.
|
|
186
|
+
#
|
|
187
|
+
# A retry RE-PRODUCES a record carrying the new count, because a Kafka
|
|
188
|
+
# record is immutable and carries no delivery counter. The offset is
|
|
189
|
+
# committed LAST so a crash in between redelivers rather than loses -
|
|
190
|
+
# at-least-once, which is the contract.
|
|
191
|
+
def fail(job, error = "")
|
|
192
|
+
job.attempts += 1
|
|
193
|
+
job.error = error
|
|
194
|
+
if job.attempts >= @max_retries
|
|
195
|
+
dead_letter(job)
|
|
196
|
+
else
|
|
197
|
+
enqueue(job)
|
|
198
|
+
end
|
|
199
|
+
acknowledge(job)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def retry(job, delay_seconds: 0)
|
|
203
|
+
if delay_seconds.to_f > 0
|
|
204
|
+
raise NotImplementedError,
|
|
205
|
+
"The kafka queue backend cannot honour retry(delay_seconds): " \
|
|
206
|
+
"Kafka has no per-message delay, for the same reason " \
|
|
207
|
+
"push(delay_seconds) is refused - a consumer reads a partition " \
|
|
208
|
+
"in offset order, so a delayed record stalls every record behind " \
|
|
209
|
+
"it. Re-producing immediately while silently dropping the delay " \
|
|
210
|
+
"would run the job far sooner than asked. Use the file or mongodb " \
|
|
211
|
+
"backend for delayed retries."
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
job.attempts += 1
|
|
215
|
+
job.error = nil
|
|
216
|
+
enqueue(job)
|
|
217
|
+
acknowledge(job)
|
|
218
|
+
true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Dead-lettered jobs, read back from the <topic>.dead_letter topic this
|
|
222
|
+
# backend produces to itself. Kafka's log is not queryable by job state,
|
|
223
|
+
# but it IS readable from the beginning - so this ANSWERS rather than
|
|
224
|
+
# refusing, and a dead-letter handler written against the file backend
|
|
225
|
+
# finds the same jobs here (invariant 3).
|
|
226
|
+
#
|
|
227
|
+
# Uses a short-lived consumer with a UNIQUE group id and never commits, so
|
|
228
|
+
# it always reads from the start and a read never consumes. Reading with
|
|
229
|
+
# the main consumer would move its offsets and steal the subscription.
|
|
230
|
+
def dead_letters(topic, max_retries: 3)
|
|
231
|
+
name = "#{topic}.dead_letter"
|
|
232
|
+
reader = Rdkafka::Config.new({
|
|
233
|
+
"bootstrap.servers" => @brokers,
|
|
234
|
+
"group.id" => "#{@group_id}.reader.#{Process.pid}.#{object_id}",
|
|
235
|
+
"auto.offset.reset" => "earliest",
|
|
236
|
+
"enable.auto.commit" => "false"
|
|
237
|
+
}.merge(@security)).consumer
|
|
238
|
+
out = []
|
|
239
|
+
begin
|
|
240
|
+
reader.subscribe(name)
|
|
241
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) +
|
|
242
|
+
(ENV["TINA4_KAFKA_ASSIGN_TIMEOUT"] || "15").to_f
|
|
243
|
+
loop do
|
|
244
|
+
msg = reader.poll(500)
|
|
245
|
+
if msg
|
|
246
|
+
data = JSON.parse(msg.payload)
|
|
247
|
+
data["status"] = "dead"
|
|
248
|
+
out << data
|
|
249
|
+
next
|
|
250
|
+
end
|
|
251
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
252
|
+
end
|
|
253
|
+
rescue Rdkafka::RdkafkaError
|
|
254
|
+
# An absent topic means nothing has been dead-lettered yet.
|
|
255
|
+
nil
|
|
256
|
+
ensure
|
|
257
|
+
reader.close
|
|
258
|
+
end
|
|
259
|
+
out
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def failed(_topic, max_retries: 3)
|
|
263
|
+
raise NotImplementedError,
|
|
264
|
+
"The kafka queue backend cannot answer failed(): a job that failed " \
|
|
265
|
+
"but is still retryable is re-produced to the main topic, and a log " \
|
|
266
|
+
"cannot be queried by job state, so it cannot be told apart from a " \
|
|
267
|
+
"normal pending record. Returning an empty list would claim nothing " \
|
|
268
|
+
"has failed. Use dead_letters() for exhausted jobs, or the file or " \
|
|
269
|
+
"mongodb backend to enumerate retryable failures."
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def retry_failed(_topic, max_retries: 3)
|
|
273
|
+
raise NotImplementedError,
|
|
274
|
+
"The kafka queue backend cannot perform retry_failed(): it must " \
|
|
275
|
+
"first enumerate the failed-but-retryable jobs, which a log cannot " \
|
|
276
|
+
"be queried for. Returning 0 would claim nothing needed retrying. " \
|
|
277
|
+
"Use retry(job_id) with an id you already hold, or the file or " \
|
|
278
|
+
"mongodb backend."
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def retry_job(_topic, job_id: nil, delay_seconds: 0)
|
|
282
|
+
raise NotImplementedError,
|
|
283
|
+
"The kafka queue backend cannot perform retry(job_id): a log cannot " \
|
|
284
|
+
"retract a record, so the dead letter would stay on the " \
|
|
285
|
+
".dead_letter topic and be revived again on the next call, " \
|
|
286
|
+
"duplicating the job. Re-produce it yourself with push() if that " \
|
|
287
|
+
"is what you want, or use the file or mongodb backend."
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def purge(_topic, _status)
|
|
291
|
+
raise NotImplementedError,
|
|
292
|
+
"The kafka queue backend cannot perform purge(): a log cannot " \
|
|
293
|
+
"delete records by status - retention is time- and size-based and " \
|
|
294
|
+
"set on the topic. Returning 0 would claim nothing needed purging. " \
|
|
295
|
+
"Configure topic retention, or use the file or mongodb backend."
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def clear(_topic)
|
|
299
|
+
raise NotImplementedError,
|
|
300
|
+
"The kafka queue backend cannot perform clear(): a log cannot " \
|
|
301
|
+
"delete records on demand - retention is time- and size-based and " \
|
|
302
|
+
"set on the topic. Returning 0 would claim the queue was emptied " \
|
|
303
|
+
"when it was not. Configure topic retention, or use the file or " \
|
|
304
|
+
"mongodb backend."
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Close the rdkafka producer and consumer (leaving the consumer group).
|
|
308
|
+
#
|
|
309
|
+
# IDEMPOTENT by construction: the handles are dropped in an ensure, so a
|
|
310
|
+
# second close finds nothing and returns. Before 3.13.95 they were left
|
|
311
|
+
# set, and rdkafka raises on closing an already-closed consumer - so a
|
|
312
|
+
# shutdown path that ran twice crashed on the second pass.
|
|
128
313
|
def close
|
|
129
314
|
@producer&.close
|
|
130
315
|
@consumer&.close
|
|
316
|
+
ensure
|
|
317
|
+
@producer = nil
|
|
318
|
+
@consumer = nil
|
|
319
|
+
@subscribed_topics = []
|
|
131
320
|
end
|
|
132
321
|
end
|
|
133
322
|
end
|