Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimistic conflicts resolution mecanism #772

Merged
merged 1 commit into from
Nov 29, 2023
Merged

Conversation

yohanboniface
Copy link
Member

original code from @Biondilbiondo (Thanks! :) )

Basically:

  • when there is a conflict (i.e. someone try to save with an old ETag)
  • we compare new version with its reference version and with the latest known
  • if we see only simple case (features added both side, removal on one side…),
    then we merge
  • otherwise we raise the conflict as before

@gendy54 gendy54 mentioned this pull request May 11, 2020
@yohanboniface yohanboniface force-pushed the optimistic-merge branch 2 times, most recently from d37bee3 to be5faee Compare February 28, 2023 17:00
umap/views.py Show resolved Hide resolved
umap/views.py Outdated Show resolved Hide resolved
@yohanboniface
Copy link
Member Author

One of the keys, if we go that direction, is to keep the relevant versions while there is an editing session going, so maybe instead of keeping the n last versions, we should combine that with the versions from the last hour (but this implies some cron to delete versions, which is less KISS).

@almet
Copy link
Member

almet commented Nov 14, 2023

I've just rebased this but I'm getting an (unrelated ?) error:

______________________________________________________________ test_can_change_owner[chromium] _______________________________________________________________

map = <Map: test map>, live_server = <LiveServer listening at http://localhost:52830>, login = <function login.<locals>.do_login at 0x113f3d120>
user = <User: Joe>

    def test_can_change_owner(map, live_server, login, user):
        page = login(map.owner)
        page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
        edit_permissions = page.get_by_title("Update permissions and editors")
        edit_permissions.click()
        close = page.locator(".umap-field-owner .close")
        close.click()
        input = page.locator("input.edit-owner")
        input.type(user.username)
        input.press("Tab")
        save = page.get_by_role("button", name="Save")
        expect(save).to_be_visible()
        save.click()
        sleep(1)  # Let save ajax go
        modified = Map.objects.get(pk=map.pk)
>       assert modified.owner == user
E       assert <User: Gabriel> == <User: Joe>
E         +<User: Gabriel>
E         -<User: Joe>

Do you have an idea where this could come from?

@almet
Copy link
Member

almet commented Nov 14, 2023

Happens also on the latest master, I've opened an issue #1404

@almet
Copy link
Member

almet commented Nov 22, 2023

I've pushed an updated version of the code and tests.

It's still doing some "magic" stuff with the form that I'm not super happy with, but I would gladly see a review of these changes already.

if removed not in latest:
raise ConflictError()

merged = latest[:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might actually do a copy.deepcopy here to avoid editing objects elsewhere.

umap/views.py Outdated
response["Last-Modified"] = self.last_modified
return response
def has_been_modified_since(self, if_unmodified_since):
if if_unmodified_since and self.last_modified != if_unmodified_since:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could just return if_unmodified_since and self.last_modified != if_unmodified_since

if not merged:
return HttpResponse(status=412)

# Replace the uploaded file by the merged version.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this "let's overwrite what the client sent us", do you have a better idea?

Copy link
Member Author

@yohanboniface yohanboniface left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me!
Didn't you said you wanted to use sets instead ?

umap/tests/test_datalayer_views.py Outdated Show resolved Hide resolved
umap/utils.py Outdated Show resolved Hide resolved
umap/views.py Outdated Show resolved Hide resolved
umap/views.py Outdated Show resolved Hide resolved
@almet
Copy link
Member

almet commented Nov 23, 2023

Didn't you said you wanted to use sets instead ?

Actually, sets needs to have hashable elements inside, which is not our case at the moment, because we're storying dicts. and dicts of lists, so it's not possible :-)

@almet almet force-pushed the optimistic-merge branch 5 times, most recently from a9f80d4 to 4f32c6c Compare November 24, 2023 22:45
@almet
Copy link
Member

almet commented Nov 29, 2023

There is some magic done with the form handling here. It's useful in order to circumvent the way we use Django model forms.

Changing this would probably mean using a separated "update datalayer form", not tied to the model, so we can validate the form without altering the model (which seem to be how django does).

Here are some notes for later usage if needed:


While trying to change how the Form is used in a DataLayer update, I've understood how things work, so here are some notes.

I wanted the submitted form to be processed and validated, and then merged with a document already present in the database.

However, since the form is a ModelForm, it must be linked to something in the database which doesn't seem to be the case here. On data validation the FileField is considered a new field, meaning that the geojson object is overwritten by the new one, and self.path, being derived from self.object.geojson is updated.

The server tries to merge conflicting saves of
the same layer.

What it does:

- use the `If-Unmodified-Since` header to check
  if changes happened to the stored data ;
- Compare the incoming version with its reference version
  to get a diff.
- Reapply the diff on top of the latest version.
- If the merge is not possible, return a
  "422 Conflict" HTTP response.
- If the merge worked, return the merged document,
  to be updated by the client.
@yohanboniface yohanboniface merged commit 9b28a48 into master Nov 29, 2023
11 checks passed
@yohanboniface yohanboniface deleted the optimistic-merge branch November 29, 2023 16:56
@yohanboniface
Copy link
Member Author

BAM! 🎉 🚀

yohanboniface added a commit that referenced this pull request Oct 2, 2024
Bumps [psycopg](https://github.com/psycopg/psycopg) from 3.2.2 to 3.2.3.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/psycopg/psycopg/blob/master/docs/news.rst">psycopg's
changelog</a>.</em></p>
<blockquote>
<p>.. currentmodule:: psycopg</p>
<p>.. index::
single: Release notes
single: News</p>
<h1><code>psycopg</code> release notes</h1>
<h2>Current release</h2>
<p>Psycopg 3.2.3
^^^^^^^^^^^^^</p>
<ul>
<li>Release binary packages including PostgreSQL 17 libpq
(:ticket:<code>[#852](https://github.com/psycopg/psycopg/issues/852)</code>).</li>
</ul>
<p>Psycopg 3.2.2
^^^^^^^^^^^^^</p>
<ul>
<li>Drop <code>!TypeDef</code> specifications as string from public
modules, as they cannot
be composed by users as <code>!typing</code> objects previously could
(:ticket:<code>[#860](https://github.com/psycopg/psycopg/issues/860)</code>).</li>
<li>Release Python 3.13 binary packages.</li>
</ul>
<p>Psycopg 3.2.1
^^^^^^^^^^^^^</p>
<ul>
<li>Fix packaging metadata breaking <code>[c]</code>,
<code>[binary]</code> dependencies

(:ticket:<code>[#853](https://github.com/psycopg/psycopg/issues/853)</code>).</li>
</ul>
<h2>Psycopg 3.2</h2>
<p>.. rubric:: New top-level features</p>
<ul>
<li>Add support for integer, floating point, boolean <code>NumPy scalar
types</code>__

(:ticket:<code>[#332](https://github.com/psycopg/psycopg/issues/332)</code>).</li>
<li>Add <code>!timeout</code> and <code>!stop_after</code> parameters to
<code>Connection.notifies()</code>
(:ticket:<code>340</code>).</li>
<li>Allow dumpers to return <code>!None</code>, to be converted to NULL
(:ticket:<code>[#377](https://github.com/psycopg/psycopg/issues/377)</code>).</li>
<li>Add :ref:<code>raw-query-cursors</code> to execute queries using
placeholders in
PostgreSQL format (<code>$1</code>, <code>$2</code>...)
(🎟️<code>[#560](psycopg/psycopg#560),
[#839](https://github.com/psycopg/psycopg/issues/839)</code>).</li>
<li>Add <code>capabilities</code> object to :ref:<code>inspect the libpq
capabilities &lt;capabilities&gt;</code>
(🎫<code>[#772](https://github.com/psycopg/psycopg/issues/772)</code>).</li>
<li>Add <code>~rows.scalar_row</code> to return scalar values from a
query
(:ticket:<code>[#723](https://github.com/psycopg/psycopg/issues/723)</code>).</li>
<li>Add <code>~Connection.cancel_safe()</code> for encrypted and
non-blocking cancellation
when using libpq v17. Use such method internally to implement</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/psycopg/psycopg/commit/ce8f07321b8d0f0d95043596cbbbb6fd1e5831e7"><code>ce8f073</code></a>
chore: bump psycopg package version to 3.2.3</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/9af9267bdbe9075a7eacf5d3997512513a762022"><code>9af9267</code></a>
Merge pull request <a
href="https://redirect.github.com/psycopg/psycopg/issues/917">#917</a>
from psycopg/pg17</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/9c9369bea40ed20cf54241d9e04693fc2426a9ff"><code>9c9369b</code></a>
docs: mention PostgreSQL 17 in binary packages in the news file</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/d0b1a3ab0a53b8bc34ba52edbe0340fcc5b0db1f"><code>d0b1a3a</code></a>
ci: install flex to build libpq</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/17e8d85f695947ef3421ff4c596100124fa2c8c1"><code>17e8d85</code></a>
ci(macos): fix dylib path for postgres 17 from brew</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/d06613299aee124be6e673b238bbe747bad25978"><code>d066132</code></a>
ci(macos): update brew to install PostgreSQL 17</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/2cc362e4dc662413f32d9e289afa0493fb2aada2"><code>2cc362e</code></a>
ci: bump to PostgreSQL 17 in binary packages</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/810bfcf09c452e378adc23a2795736dea8395cfa"><code>810bfcf</code></a>
chore: add PostgreSQL 17 TRANSACTION_TIMEOUT error</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/2a0242163b144153a6c7b9af08c47c9f3fd790f4"><code>2a02421</code></a>
ci: Add PostgreSQL 17 to CI test grid, remove PostgreSQL 11</li>
<li><a
href="https://github.com/psycopg/psycopg/commit/bea783d394ab5ad1f9f795af22a77370fad28ab6"><code>bea783d</code></a>
fix(windows): resolve absolute path to libpq</li>
<li>Additional commits viewable in <a
href="https://github.com/psycopg/psycopg/compare/3.2.2...3.2.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=psycopg&package-manager=pip&previous-version=3.2.2&new-version=3.2.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging this pull request may close these issues.

3 participants