diff --git a/CHANGELOG.md b/CHANGELOG.md index 162a0684ff..e0f5d1cefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ UNRELEASED * [ [#1989](https://github.com/digitalfabrik/integreat-cms/issues/1989) ] Improve load time of locations endpoint * [ [#1578](https://github.com/digitalfabrik/integreat-cms/issues/1578) ] Add option to hide files in global media library * [ [#1752](https://github.com/digitalfabrik/integreat-cms/issues/1752) ] Show chapter for internal link suggestions +* [ [#1518](https://github.com/digitalfabrik/integreat-cms/issues/1518) ] Prevent database corruption when moving 2 pages at the same time 2023.2.0 diff --git a/Pipfile b/Pipfile index 0efa435c70..805c034bfe 100644 --- a/Pipfile +++ b/Pipfile @@ -29,6 +29,7 @@ twine = "*" [packages] integreat-cms = {editable = true, path = "."} +django-db-mutex = "*" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index 3fd74b420e..3be1d5234d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b5ee7f98f76daedb5e5b92e8d12ba8a8e0bf8890f4db84deba904cfe0dd7e20e" + "sha256": "1715c4c8cb6049cbbb98afcc0f5a885ebd2c9b6f5df357e83c20fbd860472c01" }, "pipfile-spec": 6, "requires": { @@ -506,6 +506,14 @@ "markers": "python_version >= '3.7'", "version": "==3.13.0" }, + "django-db-mutex": { + "hashes": [ + "sha256:a2c882ec677b80b931a33ae902fe347a634de64a43b5ac3da4e5d64230ae2afe", + "sha256:dadbd6fdae070749ddbb76a86b9fb68a5e45228b366f707868267468f1aad774" + ], + "index": "pypi", + "version": "==3.0.0" + }, "django-debug-toolbar": { "hashes": [ "sha256:24ef1a7d44d25e60d7951e378454c6509bf536dce7e7d9d36e7c387db499bc27", @@ -1036,45 +1044,45 @@ }, "pydantic": { "hashes": [ - "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72", - "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423", - "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f", - "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c", - "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06", - "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53", - "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774", - "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6", - "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c", - "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f", - "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6", - "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3", - "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817", - "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903", - "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a", - "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e", - "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d", - "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85", - "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00", - "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28", - "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3", - "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024", - "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4", - "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e", - "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d", - "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa", - "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854", - "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15", - "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648", - "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8", - "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c", - "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857", - "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f", - "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416", - "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978", - "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d" + "sha256:1fd326aff5d6c36f05735c7c9b3d5b0e933b4ca52ad0b6e4b38038d82703d35b", + "sha256:2185a3b3d98ab4506a3f6707569802d2d92c3a7ba3a9a35683a7709ea6c2aaa2", + "sha256:261f357f0aecda005934e413dfd7aa4077004a174dafe414a8325e6098a8e419", + "sha256:305d0376c516b0dfa1dbefeae8c21042b57b496892d721905a6ec6b79494a66d", + "sha256:3257bd714de9db2102b742570a56bf7978e90441193acac109b1f500290f5718", + "sha256:3353072625ea2a9a6c81ad01b91e5c07fa70deb06368c71307529abf70d23325", + "sha256:36e44a4de37b8aecffa81c081dbfe42c4d2bf9f6dff34d03dce157ec65eb0f15", + "sha256:3bb99cf9655b377db1a9e47fa4479e3330ea96f4123c6c8200e482704bf1eda2", + "sha256:3f9d9b2be177c3cb6027cd67fbf323586417868c06c3c85d0d101703136e6b31", + "sha256:45edea10b75d3da43cfda12f3792833a3fa70b6eee4db1ed6aed528cef17c74e", + "sha256:51782fd81f09edcf265823c3bf43ff36d00db246eca39ee765ef58dc8421a642", + "sha256:532e97c35719f137ee5405bd3eeddc5c06eb91a032bc755a44e34a712420daf3", + "sha256:58e41dd1e977531ac6073b11baac8c013f3cd8706a01d3dc74e86955be8b2c0c", + "sha256:5920824fe1e21cbb3e38cf0f3dd24857c8959801d1031ce1fac1d50857a03bfb", + "sha256:5f3bc8f103b56a8c88021d481410874b1f13edf6e838da607dcb57ecff9b4594", + "sha256:63200cd8af1af2c07964546b7bc8f217e8bda9d0a2ef0ee0c797b36353914984", + "sha256:663d2dd78596c5fa3eb996bc3f34b8c2a592648ad10008f98d1348be7ae212fb", + "sha256:6a4b0aab29061262065bbdede617ef99cc5914d1bf0ddc8bcd8e3d7928d85bd6", + "sha256:6bb0452d7b8516178c969d305d9630a3c9b8cf16fcf4713261c9ebd465af0d73", + "sha256:72ef3783be8cbdef6bca034606a5de3862be6b72415dc5cb1fb8ddbac110049a", + "sha256:76c930ad0746c70f0368c4596020b736ab65b473c1f9b3872310a835d852eb19", + "sha256:7c5b94d598c90f2f46b3a983ffb46ab806a67099d118ae0da7ef21a2a4033b28", + "sha256:7ce1612e98c6326f10888df951a26ec1a577d8df49ddcaea87773bfbe23ba5cc", + "sha256:8481dca324e1c7b715ce091a698b181054d22072e848b6fc7895cd86f79b4449", + "sha256:87f831e81ea0589cd18257f84386bf30154c5f4bed373b7b75e5cb0b5d53ea87", + "sha256:9a9d9155e2a9f38b2eb9374c88f02fd4d6851ae17b65ee786a87d032f87008f8", + "sha256:9e337ac83686645a46db0e825acceea8e02fca4062483f40e9ae178e8bd1103a", + "sha256:b429f7c457aebb7fbe7cd69c418d1cd7c6fdc4d3c8697f45af78b8d5a7955760", + "sha256:b473d00ccd5c2061fd896ac127b7755baad233f8d996ea288af14ae09f8e0d1e", + "sha256:bd46a0e6296346c477e59a954da57beaf9c538da37b9df482e50f836e4a7d4bb", + "sha256:c428c0f64a86661fb4873495c4fac430ec7a7cef2b8c1c28f3d1a7277f9ea5ab", + "sha256:c9e5b778b6842f135902e2d82624008c6a79710207e28e86966cd136c621bfee", + "sha256:ca9075ab3de9e48b75fa8ccb897c34ccc1519177ad8841d99f7fd74cf43be5bf", + "sha256:f582cac9d11c227c652d3ce8ee223d94eb06f4228b52a8adaafa9fa62e73d5c9", + "sha256:f5bee6c523d13944a1fdc6f0525bc86dbbd94372f17b83fa6331aabacc8fd08e", + "sha256:f836444b4c5ece128b23ec36a446c9ab7f9b0f7981d0d27e13a7c366ee163f8a" ], "markers": "python_version >= '3.7'", - "version": "==1.10.4" + "version": "==1.10.5" }, "pyhanko": { "hashes": [ @@ -1365,11 +1373,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "markers": "python_version >= '3.7'", - "version": "==4.4.0" + "version": "==4.5.0" }, "tzdata": { "hashes": [ @@ -1405,10 +1413,10 @@ }, "webauthn": { "hashes": [ - "sha256:d5f5c257a6a18c6bd5a67169e62c1a2cd7a7350a6f9cd1ced02919706cef3074", - "sha256:f5122b9d988fe20116b3712c874a97183d18c0f2d62287c979edf1892d198b57" + "sha256:4153dd245dcc44f948f2543a726cbc852353d7c5bc04e7e35c6e05dc744b9a11", + "sha256:d4821ce47d9870a94bb55c23002c4e1969fb1c08ad35e878f4b8cec71e507a1d" ], - "version": "==1.7.0" + "version": "==1.7.2" }, "webencodings": { "hashes": [ @@ -1803,9 +1811,7 @@ "version": "==0.4.6" }, "coverage": { - "extras": [ - "toml" - ], + "extras": [], "hashes": [ "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab", "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851", @@ -2662,7 +2668,7 @@ "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f", "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==13.3.1" }, "secretstorage": { @@ -2675,11 +2681,11 @@ }, "setuptools": { "hashes": [ - "sha256:23c86b4e44432bfd8899384afc08872ec166a24f48a3f99f293b0a557e6a6b5d", - "sha256:daec07fd848d80676694d6bf69c009d28910aeece68a38dbe88b7e1bb6dba12e" + "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012", + "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48" ], "markers": "python_version >= '3.5'", - "version": "==67.3.1" + "version": "==67.3.2" }, "shellcheck-py": { "hashes": [ @@ -2735,7 +2741,7 @@ "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e" ], - "markers": "python_version >= '3.8'", + "markers": "python_full_version >= '3.8.0'", "version": "==1.0.4" }, "sphinxcontrib-devhelp": { @@ -2759,7 +2765,7 @@ "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903" ], - "markers": "python_version >= '3.8'", + "markers": "python_full_version >= '3.8.0'", "version": "==2.0.1" }, "sphinxcontrib-jquery": { @@ -2859,11 +2865,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "markers": "python_version >= '3.7'", - "version": "==4.4.0" + "version": "==4.5.0" }, "urllib3": { "hashes": [ @@ -2967,11 +2973,11 @@ }, "zipp": { "hashes": [ - "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", - "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" + "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7", + "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb" ], "markers": "python_version >= '3.7'", - "version": "==3.13.0" + "version": "==3.14.0" } } } diff --git a/integreat_cms/cms/models/abstract_tree_node.py b/integreat_cms/cms/models/abstract_tree_node.py index 8240491bed..c6686d8bd4 100644 --- a/integreat_cms/cms/models/abstract_tree_node.py +++ b/integreat_cms/cms/models/abstract_tree_node.py @@ -8,6 +8,9 @@ from treebeard.exceptions import InvalidPosition from treebeard.ns_tree import NS_Node +from db_mutex import DBMutexError, DBMutexTimeoutError +from db_mutex.db_mutex import db_mutex + from .abstract_base_model import AbstractBaseModel from ..constants import position @@ -259,6 +262,8 @@ def move(self, target, pos=None): :type pos: str :raises ~treebeard.exceptions.InvalidPosition: If the node is moved to another region + :raises ~db_mutex.exceptions.DBMutexError: If the DB mutex could not be retrieved + :raises ~db_mutex.exceptions.DBMutexTimeoutError: If waiting for the DB mutex timed out """ logger.debug("Moving %r to position %r of %r", self, pos, target) # Do not allow to move a node outside its region @@ -282,7 +287,21 @@ def move(self, target, pos=None): # Moving a node can modify all other nodes via raw sql queries (which are not recognized by cachalot), # so we have to invalidate the whole model manually. invalidate_model(self.__class__) - super().move(target=target, pos=pos) + try: + with db_mutex(f"tree-page-{self.region}"): + super().move(target, pos) + except DBMutexError as e: + logger.error("Could not retrieve database mutex to save %s", self) + raise DBMutexError( + # pylint: disable=consider-using-f-string + _('Could not change position in tree of "{}".'.format(self)) + ) from e + except DBMutexTimeoutError as e: + logger.error("Retrieving database mutex timed out while saving %s", self) + raise DBMutexTimeoutError( + # pylint: disable=consider-using-f-string + _('Could not change position in tree of "{}".'.format(self)) + ) from e invalidate_model(self.__class__) # Reload 'self' because lft/rgt may have changed diff --git a/integreat_cms/core/settings.py b/integreat_cms/core/settings.py index 8ac97e6800..f3c2ce20c6 100644 --- a/integreat_cms/core/settings.py +++ b/integreat_cms/core/settings.py @@ -247,6 +247,7 @@ "django.contrib.staticfiles", # Installed third-party-apps "corsheaders", + "db_mutex", "linkcheck", "polymorphic", "rules.apps.AutodiscoverRulesConfig", @@ -1012,3 +1013,11 @@ #: The URL path where XLIFF files are served for download XLIFF_URL = "/xliff/" + + +############ +# DB Mutex # +############ + +# Our database operations should never exceed 60 seconds +DB_MUTEX_TTL_SECONDS = 60 diff --git a/integreat_cms/locale/de/LC_MESSAGES/django.po b/integreat_cms/locale/de/LC_MESSAGES/django.po index f15c1f8b48..a14369c5cb 100644 --- a/integreat_cms/locale/de/LC_MESSAGES/django.po +++ b/integreat_cms/locale/de/LC_MESSAGES/django.po @@ -2340,6 +2340,10 @@ msgid "The node \"{}\" in region \"{}\" cannot be moved to \"{}\"." msgstr "" "Der Knoten \"{}\" in Region \"{}\" kann nicht nach \"{}\" verschoben werden." +#: cms/models/abstract_tree_node.py +msgid "Could not change position in tree of \"{}\"." +msgstr "Position von \"{}\" im Baum konnte nicht geƤndert werden." + #: cms/models/chat/chat_message.py msgid "sender" msgstr "Absender:in" diff --git a/sphinx/conf.py b/sphinx/conf.py index 7d1131374d..69bad0dfa6 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -93,6 +93,7 @@ "https://sphinx-rtd-tutorial.readthedocs.io/en/latest/", None, ), + "db_mutex": ("https://django-db-mutex.readthedocs.io/en/latest/", None), "django": ( f"https://docs.djangoproject.com/en/{django_version}/", f"https://docs.djangoproject.com/en/{django_version}/_objects/",