from __future__ import print_function

import psycopg2, psycopg2.extensions
import threading
import gc
import time
import sys

# inherit psycopg2 connection class just so that
# garbage collector enters the tp_clear code path
# in delete_garbage()

class my_connection(psycopg2.extensions.connection):
    pass

class db_user(threading.Thread):
    def run(self):
        conn2 = psycopg2.connect(sys.argv[1], connection_factory=my_connection)
        cursor = conn2.cursor()
        cursor.execute("UPDATE test_psycopg2_dealloc SET a = 3", async=1)

        # the conn2 desctructor will block indefinitely
        # on the completion of the query
        # (and it will not be holding the GIL during that time)
        print("begin conn2 del", file=sys.stderr)
        del cursor, conn2
        print("end conn2 del", file=sys.stderr)

def main():
    # lock out a db row
    conn1 = psycopg2.connect(sys.argv[1], connection_factory=my_connection)
    cursor = conn1.cursor()
    cursor.execute("DROP TABLE IF EXISTS test_psycopg2_dealloc")
    cursor.execute("CREATE TABLE test_psycopg2_dealloc (a int)")
    cursor.execute("INSERT INTO test_psycopg2_dealloc VALUES (1)")
    conn1.commit()
    cursor.execute("UPDATE test_psycopg2_dealloc SET a = 2", async=1)

    # concurrent thread trying to access the locked row
    db_user().start()

    # eventually, a gc.collect run will happen
    # while the conn2 is inside conn_close()
    # but this second dealloc won't get blocked
    # as it will avoid conn_close()
    for i in range(10):
        if gc.collect():
            print("garbage collection done", file=sys.stderr)
            break
        time.sleep(1)

    # we now unlock the row by invoking
    # the desctructor of conn1. This will permit the
    # concurrent thread destructor of conn2 to
    # continue and it will end up trying to free
    # self->dsn a second time.
    print("begin conn1 del", file=sys.stderr)
    del cursor, conn1
    print("end conn1 del", file=sys.stderr)


if __name__ == '__main__':
    main()