kinto icon indicating copy to clipboard operation
kinto copied to clipboard

Another (relatively minor) race condition

Open glasserc opened this issue 7 years ago • 2 comments

This program:

"""
Another race-trash script that tries to race delete-bucket with checking for some records
"""
import aiohttp
import asyncio
import async_timeout
import base64
import psycopg2

AUTH = 'Basic ' + base64.b64encode(b'test:test').decode('utf-8')
BUCKET = 'http://localhost:8888/v1/buckets/default'
COLLECTION = BUCKET + '/collections/storage-sync-crypto'
RECORDS_URL = COLLECTION + '/records?_since=0'
A_CERTAIN_RECORD = COLLECTION + '/records/keys'

async def get_new_records(session):
    await asyncio.sleep(.01)    # You might have to modify this number to taste -- I just chose numbers until I saw the delete happen before/after the get in the output
    with async_timeout.timeout(10):
        async with session.get(RECORDS_URL) as response:
            resp = await response.json()
            print(resp)
            assert response.status == 200


async def put_record(session):
    with async_timeout.timeout(10):
        async with session.put(A_CERTAIN_RECORD, data='{"data": {"a": 1}}', headers=[("Content-Type", "application/json")]) as response:
            resp = await response.json()
            print(resp)
            assert response.status < 300


async def delete_bucket(session):
    async with session.delete(BUCKET) as response:
        text = await response.text()
        print(text)
        assert response.status == 200


def check_both_deleted_and_records(conn):
    cur = conn.cursor()
    cur.execute("SELECT * FROM records r JOIN deleted d ON (r.id = d.id AND r.parent_id = d.parent_id AND r.collection_id = d.collection_id);")
    assert not cur.fetchone()
    cur.close()


async def main():
    conn = psycopg2.connect("dbname=postgres user=postgres password=postgres host=localhost")
    async with aiohttp.ClientSession(headers={"Authorization": AUTH}) as session1:
        await put_record(session1)
        things = [
            delete_bucket(session1),
            get_new_records(session2),
        ]
        await asyncio.gather(*things)
        check_both_deleted_and_records(conn)
    conn.close()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Eventually fails:

{'permissions': {'write': ['basicauth:44ac0e1157457b4ff75a88b490cd735f73a27d5c8ac5b3fb4672d449969d88ea']}, 'data': {'a': 1, 'id': 'keys', 'last_modified': 1511369823965}}
{"data":{"last_modified":1511369824042,"id":"3f2e55d2-25ad-5fa9-3dcb-c0f29235627f","deleted":true}}
{'code': 403, 'errno': 121, 'error': 'Forbidden', 'message': 'This user cannot access this resource.'}
Traceback (most recent call last):
  File "../race-trash-2.py", line 61, in <module>
    loop.run_until_complete(main())
  File "/usr/lib64/python3.6/asyncio/base_events.py", line 467, in run_until_complete
    return future.result()
  File "../race-trash-2.py", line 56, in main
    await asyncio.gather(*things)
  File "../race-trash-2.py", line 22, in get_new_records
    assert response.status == 200
AssertionError

In the logs:

2017-11-22 11:57:04,127 WARNI [kinto.core.authorization][waitress] Permission 'read' on '/buckets/3f2e55d2-25ad-5fa9-3dcb-c0f29235627f/collections/storage-sync-crypto/records' not granted to 'basicauth:44ac0e1157457b4ff75a88b490cd735f73a27d5c8ac5b3fb4672d449969d88ea'.

glasserc avatar Nov 22 '17 16:11 glasserc

More generally, this is due to the fact that the storage and the permission backends are split.

We could investigate about 2-phase commits

leplatrem avatar Nov 30 '17 14:11 leplatrem

I could be wrong, but I wonder if 2-phase commit would even be helpful here -- the problem isn't that some transactions succeed and some don't, the problem is that with READ COMMITTED we can get changes in the middle of our transactions.

glasserc avatar Dec 04 '17 17:12 glasserc