Use Celery instead of RQ.

Celery provides a more robust set of queueing options for both tasks and
worker processes. Updates include:
- infrastructure necessary to run Celery, including celery entrypoint
- backgrounded functions are now imported directly from atst.jobs
- update tests as-needed
- update kubernetes worker pod command
This commit is contained in:
dandds 2019-08-22 11:55:00 -04:00
parent b7f8152cc1
commit d7478e322a
18 changed files with 209 additions and 191 deletions

View File

@ -20,12 +20,12 @@ pyopenssl = "*"
requests = "*" requests = "*"
apache-libcloud = "*" apache-libcloud = "*"
lockfile = "*" lockfile = "*"
"flask-rq2" = "*"
werkzeug = "*" werkzeug = "*"
PyYAML = "*" PyYAML = "*"
azure-storage = "*" azure-storage = "*"
azure-storage-common = "*" azure-storage-common = "*"
boto3 = "*" boto3 = "*"
celery = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"

191
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "f09d90a1b4b86eff2e0ed453ceefcce4f19d54c57fb50ebed1f02aae8ab83c2a" "sha256": "bf3b598c052193f70249da97ac746bc53aeb72f45a4515e10945a4274aba7b18"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -18,10 +18,17 @@
"default": { "default": {
"alembic": { "alembic": {
"hashes": [ "hashes": [
"sha256:cdb7d98bd5cbf65acd38d70b1c05573c432e6473a82f955cdea541b5c153b0cc" "sha256:4a4811119efbdc5259d1f4c8f6de977b36ad3bcc919f59a29c2960c5ef9149e4"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.0.11" "version": "==1.1.0"
},
"amqp": {
"hashes": [
"sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68",
"sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a"
],
"version": "==2.5.1"
}, },
"apache-libcloud": { "apache-libcloud": {
"hashes": [ "hashes": [
@ -69,20 +76,35 @@
"index": "pypi", "index": "pypi",
"version": "==2.1.0" "version": "==2.1.0"
}, },
"billiard": {
"hashes": [
"sha256:01afcb4e7c4fd6480940cfbd4d9edc19d7a7509d6ada533984d0d0f49901ec82",
"sha256:b8809c74f648dfe69b973c8e660bcec00603758c9db8ba89d7719f88d5f01f26"
],
"version": "==3.6.1.0"
},
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:3ec5b520dbd0a430cdd581a8250991fb0f21ee7e668a8928f15006b312fa86dc", "sha256:366a1f3ec37b9434f25247cbe876f9ca1b53d35e35af18f74c735445100b4bc4",
"sha256:8aec0247131a0db1e33d28ad13910e01e6dfa208e8ab8ee5a4095e92dbaabf45" "sha256:e7718b48cd073ad59a99a33d14252319dfaf550be3682b0c6a58da052fb05fcc"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.9.204" "version": "==1.9.217"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:56cd1114e0ce35733e890b321160c8c438243f4fa54d3d074dfa6bdce4ee55aa", "sha256:68a0a22ca4e0e7e7ab482f63e21debfe402841fc49b8503dec0a7307b565d774",
"sha256:f86504bcc9c44d5b2e7b019f2f279b70f17b1400d2fc4775bc009ec473530cad" "sha256:7a213b876e58b1b5380cf30faa05ba45073692ad4a3cc803ba763082a36436bb"
], ],
"version": "==1.12.204" "version": "==1.12.217"
},
"celery": {
"hashes": [
"sha256:821d11967f0f3f8fe24bd61ecfc7b6acbb5a926b719f1e8c4d5ff7bc09e18d81",
"sha256:ae4541fb3af5182bd4af749fee9b89c4858f2792d34bb5d034967e662cf9b55c"
],
"index": "pypi",
"version": "==4.4.0rc3"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@ -138,13 +160,6 @@
], ],
"version": "==7.0" "version": "==7.0"
}, },
"croniter": {
"hashes": [
"sha256:0d905dbe6f131a910fd3dde792f0129788cd2cb3a8048c5f7aaa212670b0cef2",
"sha256:538adeb3a7f7816c3cdec6db974c441620d764c25ff4ed0146ee7296b8a50590"
],
"version": "==0.3.30"
},
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c",
@ -168,11 +183,11 @@
}, },
"docutils": { "docutils": {
"hashes": [ "hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
], ],
"version": "==0.14" "version": "==0.15.2"
}, },
"flask": { "flask": {
"hashes": [ "hashes": [
@ -189,14 +204,6 @@
"index": "pypi", "index": "pypi",
"version": "==0.12" "version": "==0.12"
}, },
"flask-rq2": {
"hashes": [
"sha256:3ef6395065255447f8e1516ccca24858ba87da1d71a6975e0e3b55256bf04967",
"sha256:abe1e52d3b98abe37e85830a614ba6af864516f1b6cf2229f352f8500eafc5fd"
],
"index": "pypi",
"version": "==18.3"
},
"flask-session": { "flask-session": {
"hashes": [ "hashes": [
"sha256:a31c27e0c3287f00c825b3d9625aba585f4df4cccedb1e7dd5a69a215881a731", "sha256:a31c27e0c3287f00c825b3d9625aba585f4df4cccedb1e7dd5a69a215881a731",
@ -228,6 +235,13 @@
], ],
"version": "==2.8" "version": "==2.8"
}, },
"importlib-metadata": {
"hashes": [
"sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8",
"sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"
],
"version": "==0.19"
},
"itsdangerous": { "itsdangerous": {
"hashes": [ "hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
@ -249,6 +263,13 @@
], ],
"version": "==0.9.4" "version": "==0.9.4"
}, },
"kombu": {
"hashes": [
"sha256:55274dc75eb3c3994538b0973a0fadddb236b698a4bc135b8aa4981e0a710b8f",
"sha256:e5f0312dfb9011bebbf528ccaf118a6c2b5c3b8244451f08381fb23e7715809b"
],
"version": "==4.6.4"
},
"lockfile": { "lockfile": {
"hashes": [ "hashes": [
"sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799", "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799",
@ -296,6 +317,13 @@
], ],
"version": "==1.1.1" "version": "==1.1.1"
}, },
"more-itertools": {
"hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
],
"version": "==7.2.0"
},
"pendulum": { "pendulum": {
"hashes": [ "hashes": [
"sha256:1cde6e3c6310fb882c98f373795f807cb2bd6af01f34d2857e6e283b5ee91e09", "sha256:1cde6e3c6310fb882c98f373795f807cb2bd6af01f34d2857e6e283b5ee91e09",
@ -372,6 +400,13 @@
], ],
"version": "==1.0.4" "version": "==1.0.4"
}, },
"pytz": {
"hashes": [
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
],
"version": "==2019.2"
},
"pytzdata": { "pytzdata": {
"hashes": [ "hashes": [
"sha256:c0c8316eaf6c25ba45816390a1a45c39790767069b3275c5f7de3ddf773eb810", "sha256:c0c8316eaf6c25ba45816390a1a45c39790767069b3275c5f7de3ddf773eb810",
@ -400,11 +435,11 @@
}, },
"redis": { "redis": {
"hashes": [ "hashes": [
"sha256:45682ecf226c7611efe731974c4fa3390170ba045b9cdb26f0051114a5c2a68b", "sha256:98a22fb750c9b9bb46e75e945dc3f61d0ab30d06117cbb21ff9cd1d315fedd3b",
"sha256:f2609a85e5f37f489ba3b5652e1175dc3711c4d7a7818c4f657615810afd23df" "sha256:c504251769031b0dd7dd5cf786050a6050197c6de0d37778c80c08cb04ae8275"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.3.6" "version": "==3.3.8"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
@ -414,20 +449,6 @@
"index": "pypi", "index": "pypi",
"version": "==2.22.0" "version": "==2.22.0"
}, },
"rq": {
"hashes": [
"sha256:2798d26a7b850e759f23f69695a389d676a9c08f2c14f96f0d34d9648c9d5616",
"sha256:4f27c6a690d1bd02b9157e615d8819555b9b359c0c4ec8ff0013d160c31b40bb"
],
"version": "==1.1.0"
},
"rq-scheduler": {
"hashes": [
"sha256:90eb3915e31cc2032c301e5ab1fd5ad6f23d9500435046995e009f098e18efbe",
"sha256:ff7d45b34a8a39c9c83634f642aeef950641c75c4eeea3fa140bf574bfc6aca2"
],
"version": "==0.9"
},
"s3transfer": { "s3transfer": {
"hashes": [ "hashes": [
"sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d", "sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d",
@ -444,10 +465,10 @@
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
"sha256:217e7fc52199a05851eee9b6a0883190743c4fb9c8ac4313ccfceaffd852b0ff" "sha256:2f8ff566a4d3a92246d367f2e9cd6ed3edeef670dcd6dda6dfdc9efed88bcd80"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.6" "version": "==1.3.8"
}, },
"unipath": { "unipath": {
"hashes": [ "hashes": [
@ -465,6 +486,13 @@
"markers": "python_version >= '3.4'", "markers": "python_version >= '3.4'",
"version": "==1.25.3" "version": "==1.25.3"
}, },
"vine": {
"hashes": [
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
],
"version": "==1.3.0"
},
"webassets": { "webassets": {
"hashes": [ "hashes": [
"sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db" "sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db"
@ -486,6 +514,13 @@
"sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1"
], ],
"version": "==2.2.1" "version": "==2.2.1"
},
"zipp": {
"hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
],
"version": "==0.6.0"
} }
}, },
"develop": { "develop": {
@ -653,10 +688,10 @@
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:96ad7902706f2409a2d0c3de5132f69b413555a419bacec99d3f16e657895b47", "sha256:1d3f700e8dfcefd6e657118d71405d53e86974448aba78884f119bbd84c0cddf",
"sha256:b3bb64aff9571510de6812df45122b633dbc6227e870edae3ed9430f94698521" "sha256:d5366e120191c5610fceeebfe1c298dc46da0277096f639c6dd7e2eaee0fa547"
], ],
"version": "==2.0.0" "version": "==2.0.1"
}, },
"flask": { "flask": {
"hashes": [ "hashes": [
@ -675,10 +710,10 @@
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:c15c55ff890cd3a6a8330059e80885410a328f645551b55a91d858bfb3eb2573", "sha256:947cc75913e7b6da108458136607e2ee0e40c20be1e12d4284e7c6c12956c276",
"sha256:df752b6b6f06f11213e91c4925aea7eaf9e37e88fb71c8a7a1aa0a5c10852120" "sha256:d2f4945f8260f6981d724f5957bc076398ada55cb5d25aaee10108bcdc894100"
], ],
"version": "==2.1.13" "version": "==3.0.2"
}, },
"honcho": { "honcho": {
"hashes": [ "hashes": [
@ -740,10 +775,10 @@
}, },
"jedi": { "jedi": {
"hashes": [ "hashes": [
"sha256:53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27",
"sha256:e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c" "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"
], ],
"version": "==0.14.1" "version": "==0.15.1"
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
@ -754,26 +789,26 @@
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
"hashes": [ "hashes": [
"sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", "sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf",
"sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", "sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3",
"sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", "sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce",
"sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", "sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f",
"sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", "sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f",
"sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", "sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0",
"sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", "sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e",
"sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", "sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905",
"sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", "sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8",
"sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", "sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2",
"sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", "sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009",
"sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", "sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a",
"sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", "sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512",
"sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", "sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5",
"sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", "sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e",
"sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", "sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4",
"sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", "sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f",
"sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" "sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1"
], ],
"version": "==1.4.1" "version": "==1.4.2"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
@ -1090,10 +1125,10 @@
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [
"sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
], ],
"version": "==0.5.2" "version": "==0.6.0"
} }
} }
} }

View File

@ -25,10 +25,10 @@ from atst.domain.authz import Authorization
from atst.domain.csp import make_csp_provider from atst.domain.csp import make_csp_provider
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.queue import celery, update_celery
from atst.utils import mailer from atst.utils import mailer
from atst.utils.form_cache import FormCache from atst.utils.form_cache import FormCache
from atst.utils.json import CustomJSONEncoder from atst.utils.json import CustomJSONEncoder
from atst.queue import queue
from atst.utils.notification_sender import NotificationSender from atst.utils.notification_sender import NotificationSender
from atst.utils.session_limiter import SessionLimiter from atst.utils.session_limiter import SessionLimiter
@ -59,13 +59,14 @@ def make_app(config):
app.config.update(config) app.config.update(config)
app.config.update({"SESSION_REDIS": app.redis}) app.config.update({"SESSION_REDIS": app.redis})
update_celery(celery, app)
make_flask_callbacks(app) make_flask_callbacks(app)
register_filters(app) register_filters(app)
make_csp_provider(app, config.get("CSP", "mock")) make_csp_provider(app, config.get("CSP", "mock"))
make_crl_validator(app) make_crl_validator(app)
make_mailer(app) make_mailer(app)
make_notification_sender(app) make_notification_sender(app)
queue.init_app(app)
db.init_app(app) db.init_app(app)
csrf.init_app(app) csrf.init_app(app)
@ -149,6 +150,7 @@ def map_config(config):
return { return {
**config["default"], **config["default"],
"ENV": config["default"]["ENVIRONMENT"], "ENV": config["default"]["ENVIRONMENT"],
"BROKER_URL": config["default"]["REDIS_URI"],
"DEBUG": config["default"].getboolean("DEBUG"), "DEBUG": config["default"].getboolean("DEBUG"),
"SQLALCHEMY_ECHO": config["default"].getboolean("SQLALCHEMY_ECHO"), "SQLALCHEMY_ECHO": config["default"].getboolean("SQLALCHEMY_ECHO"),
"CLASSIFIED": config["default"].getboolean("CLASSIFIED"), "CLASSIFIED": config["default"].getboolean("CLASSIFIED"),
@ -248,7 +250,7 @@ def make_mailer(app):
def make_notification_sender(app): def make_notification_sender(app):
app.notification_sender = NotificationSender(queue) app.notification_sender = NotificationSender()
def make_session_limiter(app, session, config): def make_session_limiter(app, session, config):

18
atst/jobs.py Normal file
View File

@ -0,0 +1,18 @@
from flask import current_app as app
from atst.queue import celery
@celery.task()
def send_mail(recipients, subject, body):
app.mailer.send(recipients, subject, body)
@celery.task()
def send_notification_mail(recipients, subject, body):
app.logger.info(
"Sending a notification to these recipients: {}\n\nSubject: {}\n\n{}".format(
recipients, subject, body
)
)
app.mailer.send(recipients, subject, body)

View File

@ -1,57 +1,15 @@
from flask_rq2 import RQ from celery import Celery
from flask import current_app as app
celery = Celery(__name__)
class ATSTQueue(RQ): def update_celery(celery, app):
celery.conf.update(app.config)
"""Internal helpers to get the queue that actually does the work. class ContextTask(celery.Task):
def __call__(self, *args, **kwargs):
with app.app_context():
return self.run(*args, **kwargs)
The RQ object always uses the "default" queue, unless we explicitly request celery.Task = ContextTask
otherwise. These helpers allow us to use `.queue_name` to get the name of return celery
the configured queue and `_queue_job` will use the appropriate queue.
"""
@property
def queue_name(self):
return self.queues[0]
def get_queue(self, name=None):
if not name:
name = self.queue_name
return super().get_queue(name)
def _queue_job(self, function, *args, **kwargs):
self.get_queue().enqueue(function, *args, **kwargs)
# pylint: disable=pointless-string-statement
"""Instance methods to queue up application-specific jobs."""
def send_mail(self, recipients, subject, body):
self._queue_job(ATSTQueue._send_mail, recipients, subject, body)
def send_notification_mail(self, recipients, subject, body):
self._queue_job(ATSTQueue._send_notification_mail, recipients, subject, body)
# pylint: disable=pointless-string-statement
"""Class methods to actually perform the work.
Must be a class method (or a module-level function) because we being able
to pickle the class is more effort than its worth.
"""
@classmethod
def _send_mail(self, recipients, subject, body):
app.mailer.send(recipients, subject, body)
@classmethod
def _send_notification_mail(self, recipients, subject, body):
app.logger.info(
"Sending a notification to these recipients: {}\n\nSubject: {}\n\n{}".format(
recipients, subject, body
)
)
app.mailer.send(recipients, subject, body)
queue = ATSTQueue()

View File

@ -14,7 +14,7 @@ from atst.forms.team import TeamForm
from atst.models import Permissions from atst.models import Permissions
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
from atst.utils.localization import translate from atst.utils.localization import translate
from atst.queue import queue from atst.jobs import send_mail
def get_form_permission_value(member, edit_perm_set): def get_form_permission_value(member, edit_perm_set):
@ -129,7 +129,7 @@ def send_application_invitation(invitee_email, inviter_name, token):
body = render_template( body = render_template(
"emails/application/invitation.txt", owner=inviter_name, token=token "emails/application/invitation.txt", owner=inviter_name, token=token
) )
queue.send_mail( send_mail.delay(
[invitee_email], [invitee_email],
translate("email.application_invite", {"inviter_name": inviter_name}), translate("email.application_invite", {"inviter_name": inviter_name}),
body, body,

View File

@ -15,7 +15,7 @@ from atst.domain.exceptions import AlreadyExistsError, NotFoundError
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.forms.data import SERVICE_BRANCHES from atst.forms.data import SERVICE_BRANCHES
from atst.queue import queue from atst.jobs import send_mail
from atst.utils import pick from atst.utils import pick
@ -174,7 +174,7 @@ def dev_new_user():
@bp.route("/test-email") @bp.route("/test-email")
def test_email(): def test_email():
queue.send_mail( send_mail.delay(
[request.args.get("to")], request.args.get("subject"), request.args.get("body") [request.args.get("to")], request.args.get("subject"), request.args.get("body")
) )

View File

@ -6,7 +6,7 @@ from atst.domain.exceptions import AlreadyExistsError
from atst.domain.invitations import PortfolioInvitations from atst.domain.invitations import PortfolioInvitations
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models import Permissions from atst.models import Permissions
from atst.queue import queue from atst.jobs import send_mail
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
from atst.utils.localization import translate from atst.utils.localization import translate
import atst.forms.portfolio_member as member_forms import atst.forms.portfolio_member as member_forms
@ -16,7 +16,7 @@ def send_portfolio_invitation(invitee_email, inviter_name, token):
body = render_template( body = render_template(
"emails/portfolio/invitation.txt", owner=inviter_name, token=token "emails/portfolio/invitation.txt", owner=inviter_name, token=token
) )
queue.send_mail( send_mail.delay(
[invitee_email], [invitee_email],
translate("email.portfolio_invite", {"inviter_name": inviter_name}), translate("email.portfolio_invite", {"inviter_name": inviter_name}),
body, body,

View File

@ -1,6 +1,6 @@
from sqlalchemy import select from sqlalchemy import select
from atst.queue import ATSTQueue from atst.jobs import send_notification_mail
from atst.database import db from atst.database import db
from atst.models import NotificationRecipient from atst.models import NotificationRecipient
@ -8,12 +8,9 @@ from atst.models import NotificationRecipient
class NotificationSender(object): class NotificationSender(object):
EMAIL_SUBJECT = "ATST notification" EMAIL_SUBJECT = "ATST notification"
def __init__(self, queue: ATSTQueue):
self.queue = queue
def send(self, body, type_=None): def send(self, body, type_=None):
recipients = self._get_recipients(type_) recipients = self._get_recipients(type_)
self.queue.send_notification_mail(recipients, self.EMAIL_SUBJECT, body) send_notification_mail.delay(recipients, self.EMAIL_SUBJECT, body)
def _get_recipients(self, type_): def _get_recipients(self, type_):
query = select([NotificationRecipient.email]) query = select([NotificationRecipient.email])

7
celery_worker.py Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env python
from atst.app import celery, make_app, make_config
config = make_config()
app = make_app(config)
app.app_context().push()

View File

@ -142,9 +142,10 @@ spec:
image: 904153757533.dkr.ecr.us-east-2.amazonaws.com/atat:884d95ada21a5097f5c07f305d8e4e24d0f2a03f image: 904153757533.dkr.ecr.us-east-2.amazonaws.com/atat:884d95ada21a5097f5c07f305d8e4e24d0f2a03f
args: [ args: [
"/opt/atat/atst/.venv/bin/python", "/opt/atat/atst/.venv/bin/python",
"/opt/atat/atst/.venv/bin/flask", "/opt/atat/atst/.venv/bin/celery",
"rq", "-A",
"worker" "celery_worker.celery",
"worker",
] ]
resources: resources:
requests: requests:

View File

@ -143,9 +143,10 @@ spec:
image: pwatat.azurecr.io/atat:884d95ada21a5097f5c07f305d8e4e24d0f2a03f image: pwatat.azurecr.io/atat:884d95ada21a5097f5c07f305d8e4e24d0f2a03f
args: [ args: [
"/opt/atat/atst/.venv/bin/python", "/opt/atat/atst/.venv/bin/python",
"/opt/atat/atst/.venv/bin/flask", "/opt/atat/atst/.venv/bin/celery",
"rq", "-A",
"worker" "celery_worker.celery",
"worker",
] ]
resources: resources:
requests: requests:

View File

@ -4,8 +4,10 @@
set -e set -e
WORKER="pipenv run celery -A celery_worker.celery worker --loglevel=info"
if [[ `command -v entr` ]]; then if [[ `command -v entr` ]]; then
find atst | entr -r flask rq worker find atst | entr -r $WORKER
else else
flask rq worker $WORKER
fi fi

View File

@ -9,7 +9,6 @@ from collections import OrderedDict
from atst.app import make_app, make_config from atst.app import make_app, make_config
from atst.database import db as _db from atst.database import db as _db
from atst.queue import queue as atst_queue
import tests.factories as factories import tests.factories as factories
from tests.mocks import PDF_FILENAME, PDF_FILENAME2 from tests.mocks import PDF_FILENAME, PDF_FILENAME2
from tests.utils import FakeLogger, FakeNotificationSender from tests.utils import FakeLogger, FakeNotificationSender
@ -158,12 +157,6 @@ def extended_financial_verification_data(pdf_upload):
} }
@pytest.fixture(scope="function", autouse=True)
def queue():
yield atst_queue
atst_queue.get_queue().empty()
@pytest.fixture @pytest.fixture
def crl_failover_open_app(app): def crl_failover_open_app(app):
app.config.update({"CRL_FAIL_OPEN": True}) app.config.update({"CRL_FAIL_OPEN": True})

View File

@ -1,10 +1,11 @@
import uuid import uuid
from unittest.mock import Mock
from flask import url_for from flask import url_for
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.models import CSPRole from atst.models import CSPRole
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
from atst.queue import queue
from tests.factories import * from tests.factories import *
@ -149,8 +150,9 @@ def test_update_team_revoke_environment_access(client, user_session, db, session
assert user not in environment.users assert user not in environment.users
def test_create_member(client, user_session, session): def test_create_member(monkeypatch, client, user_session, session):
queue_length = len(queue.get_queue()) job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user = UserFactory.create() user = UserFactory.create()
application = ApplicationFactory.create( application = ApplicationFactory.create(
environments=[{"name": "Naboo"}, {"name": "Endor"}] environments=[{"name": "Naboo"}, {"name": "Endor"}]
@ -198,7 +200,7 @@ def test_create_member(client, user_session, session):
) )
assert invitation.role.application == application assert invitation.role.application == application
assert len(queue.get_queue()) == queue_length + 1 assert job_mock.called
def test_remove_member_success(client, user_session): def test_remove_member_success(client, user_session):

View File

@ -1,10 +1,11 @@
import datetime import datetime
from unittest.mock import Mock
from flask import url_for from flask import url_for
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models import InvitationStatus, PortfolioRoleStatus from atst.models import InvitationStatus, PortfolioRoleStatus
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.queue import queue
from tests.factories import * from tests.factories import *
@ -183,7 +184,11 @@ def test_user_can_only_revoke_invites_in_their_portfolio(client, user_session):
assert not invite.is_revoked assert not invite.is_revoked
def test_user_can_only_resend_invites_in_their_portfolio(client, user_session, queue): def test_user_can_only_resend_invites_in_their_portfolio(
monkeypatch, client, user_session
):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
other_portfolio = PortfolioFactory.create() other_portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
@ -206,10 +211,12 @@ def test_user_can_only_resend_invites_in_their_portfolio(client, user_session, q
) )
assert response.status_code == 404 assert response.status_code == 404
assert len(queue.get_queue()) == 0 assert not job_mock.called
def test_resend_invitation_sends_email(client, user_session, queue): def test_resend_invitation_sends_email(monkeypatch, client, user_session):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
ws_role = PortfolioRoleFactory.create( ws_role = PortfolioRoleFactory.create(
@ -227,12 +234,14 @@ def test_resend_invitation_sends_email(client, user_session, queue):
) )
) )
assert len(queue.get_queue()) == 1 assert job_mock.called
def test_existing_member_invite_resent_to_email_submitted_in_form( def test_existing_member_invite_resent_to_email_submitted_in_form(
client, user_session, queue monkeypatch, client, user_session
): ):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = PortfolioRoleFactory.create( ws_role = PortfolioRoleFactory.create(
@ -253,10 +262,10 @@ def test_existing_member_invite_resent_to_email_submitted_in_form(
) )
) )
send_mail_job = queue.get_queue().jobs[0]
assert user.email != "example@example.com" assert user.email != "example@example.com"
assert send_mail_job.func.__func__.__name__ == "_send_mail" ordered_args, _unordered_args = job_mock.call_args
assert send_mail_job.args[0] == ["example@example.com"] recipients, _subject, _message = ordered_args
assert recipients[0] == "example@example.com"
_DEFAULT_PERMS_FORM_DATA = { _DEFAULT_PERMS_FORM_DATA = {
@ -278,11 +287,12 @@ def test_user_with_permission_has_add_member_link(client, user_session):
) )
def test_invite_member(client, user_session, session): def test_invite_member(monkeypatch, client, user_session, session):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user_data = UserFactory.dictionary() user_data = UserFactory.dictionary()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
queue_length = len(queue.get_queue())
response = client.post( response = client.post(
url_for("portfolios.invite_member", portfolio_id=portfolio.id), url_for("portfolios.invite_member", portfolio_id=portfolio.id),
@ -307,5 +317,5 @@ def test_invite_member(client, user_session, session):
) )
assert invitation.role.portfolio == portfolio assert invitation.role.portfolio == portfolio
assert len(queue.get_queue()) == queue_length + 1 assert job_mock.called
assert len(invitation.role.permission_sets) == 5 assert len(invitation.role.permission_sets) == 5

View File

@ -1,5 +0,0 @@
def test_send_mail(queue):
queue.send_mail(
["lordvader@geocities.net"], "death start", "how is it coming along?"
)
assert len(queue.get_queue()) == 1

View File

@ -6,22 +6,19 @@ from atst.utils.notification_sender import NotificationSender
@pytest.fixture @pytest.fixture
def mock_queue(queue): def notification_sender():
return Mock(spec=queue) return NotificationSender()
@pytest.fixture def test_can_send_notification(monkeypatch, notification_sender):
def notification_sender(mock_queue): job_mock = Mock()
return NotificationSender(mock_queue) monkeypatch.setattr("atst.jobs.send_notification_mail.delay", job_mock)
def test_can_send_notification(mock_queue, notification_sender):
recipient_email = "test@example.com" recipient_email = "test@example.com"
email_body = "This is a test" email_body = "This is a test"
NotificationRecipientFactory.create(email=recipient_email) NotificationRecipientFactory.create(email=recipient_email)
notification_sender.send(email_body) notification_sender.send(email_body)
mock_queue.send_notification_mail.assert_called_once_with( job_mock.assert_called_once_with(
("test@example.com",), notification_sender.EMAIL_SUBJECT, email_body ("test@example.com",), notification_sender.EMAIL_SUBJECT, email_body
) )