Changes between Initial Version and Version 1 of AdvancedApplicationDevelopment


Ignore:
Timestamp:
09/19/26 02:18:06 (12 days ago)
Author:
183164
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedApplicationDevelopment

    v1 v1  
     1= Напредно развивање на апликацијата =
     2
     3Прототипот од фазата P4 е надграден со базен на конекции и со експлицитни трансакции. Изворниот код е во git репозиториумот на проектот, во папката cityfix.
     4
     5== Трансакции ==
     6
     7Апликацијата користи конекции во режим autocommit, па секоја поединечна команда е посебна трансакција. Таму каде што една операција на корисникот бара повеќе команди кои мора да успеат заедно, командите се извршуваат во експлицитна трансакција со with conn.transaction(): ако која било команда фрли грешка (вклучувајќи ги и грешките од тригерите од фазата P7), целата трансакција автоматски се поништува, а корисникот добива порака.
     8
     9Кај операциите каде двајца корисници можат истовремено да ја менуваат истата пријава, пријавата се заклучува со SELECT ... FOR UPDATE. Втората трансакција чека додека првата не заврши, а потоа ја гледа новата состојба и безбедно се откажува.
     10
     11=== Трансакција 1 - Поднесување на пријава (cityfix/citizen.py, UC0003) ===
     12
     13Пријавата, првиот запис во историјата на статуси и сите фотографии мора да се зачуваат заедно. Ако внесот на која било фотографија не успее, не смее да остане пријава без фотографиите, ниту пријава без запис во историјата (тоа го бара и одложеното ограничување reports_status_consistency од P7).
     14
     15{{{#!python
     16try:
     17    with pool.connection() as conn:
     18        with conn.transaction():
     19            report = conn.execute(
     20                """
     21                INSERT INTO reports (description, location_text, latitude, longitude,
     22                                     category_id, citizen_id)
     23                VALUES (%s, %s, %s, %s, %s, %s)
     24                RETURNING report_id, created_at, priority
     25                """,
     26                (description, location_text, latitude, longitude,
     27                 category["category_id"], citizen["citizen_id"]),
     28            ).fetchone()
     29            conn.execute(
     30                """
     31                INSERT INTO status_logs (status, changed_at, report_id, worker_id)
     32                VALUES ('submitted', %s, %s, NULL)
     33                """,
     34                (report["created_at"], report["report_id"]),
     35            )
     36            with conn.cursor() as cur:
     37                cur.executemany(
     38                    "INSERT INTO photos (image_url, report_id) VALUES (%s, %s)",
     39                    [(f"/uploads/reports/{report['report_id']}/{p}", report["report_id"])
     40                     for p in photos],
     41                )
     42except psycopg.Error as exc:
     43    print(f"\n  Пријавата не е зачувана: {db.error_message(exc)}")
     44}}}
     45
     46Приоритетот се чита од RETURNING, бидејќи тригерот reports_before_insert (P7) може автоматски да го покачи ако истиот проблем е веќе пријавен во близина.
     47
     48=== Трансакција 2 - Конзистентен преглед на пријава (cityfix/citizen.py, UC0004) ===
     49
     50Деталите за пријавата се читаат со четири прашалници (пријава, историја, коментари, фотографии). Ако работник го смени статусот помеѓу нив, граѓанинот би можел да види статус кој не се совпаѓа со историјата. Затоа сите четири прашалници се извршуваат во една трансакција со ниво на изолација REPEATABLE READ, само за читање, па гледаат иста слика на базата.
     51
     52{{{#!python
     53with pool.connection() as conn:
     54    with conn.transaction():
     55        conn.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY")
     56        report = conn.execute("SELECT r.description, r.status, ... WHERE r.report_id = %s",
     57                              (report_id,)).fetchone()
     58        history = conn.execute("SELECT l.status, l.changed_at, ... FROM status_logs l ...",
     59                               (report_id,)).fetchall()
     60        comments = conn.execute("SELECT cm.content, ... FROM comments cm ...",
     61                                (report_id,)).fetchall()
     62        photos = conn.execute("SELECT photo_id, image_url FROM photos WHERE report_id = %s",
     63                              (report_id,)).fetchall()
     64}}}
     65
     66=== Трансакција 3 - Промена на статусот со коментар (cityfix/worker.py, UC0005) ===
     67
     68При промена на статусот работникот може да остави и коментар за граѓанинот. Коментарот мора да се внесе пред промената, бидејќи тригерот comments_before_insert (P7) не дозволува коментари на затворена пријава. Ако промената на статусот не успее, и коментарот мора да се поништи. На една пријава може да бидат доделени повеќе работници, па пријавата прво се заклучува и се проверува дека статусот не е променет во меѓувреме.
     69
     70{{{#!python
     71def change_status_tx(pool, worker_id, report_id, expected_status, new_status,
     72                     note=None, comment=None, hold_seconds=0):
     73    with pool.connection() as conn:
     74        with conn.transaction():
     75            current = conn.execute(
     76                "SELECT status FROM reports WHERE report_id = %s FOR UPDATE",
     77                (report_id,),
     78            ).fetchone()
     79            if hold_seconds:
     80                conn.execute("SELECT pg_sleep(%s)", (hold_seconds,))
     81            if current["status"] != expected_status:
     82                raise StatusConflict(
     83                    f"Пријавата во меѓувреме е променета во „{ui.STATUS[current['status']]}“. "
     84                    "Обиди се повторно.")
     85            if comment:
     86                conn.execute(
     87                    "INSERT INTO comments (content, report_id, worker_id) VALUES (%s, %s, %s)",
     88                    (comment, report_id, worker_id),
     89                )
     90            conn.execute("CALL change_report_status(%s, %s, %s, %s)",
     91                         (report_id, worker_id, new_status, note))
     92}}}
     93
     94Процедурата change_report_status и тригерите од P7 ги проверуваат дозволените премини и доделеноста на работникот и го усогласуваат статусот во reports. Параметарот hold_seconds се користи само во демонстрацијата подолу, за двете трансакции сигурно да се преклопат.
     95
     96=== Трансакција 4 - Доделување на пријава со промена на приоритет (cityfix/admin.py, UC0007) ===
     97
     98При доделување, администраторот може да го смени и приоритетот. Двете промени мора да се зачуваат заедно. Двајца администратори можат истовремено да ја гледаат листата на недоделени пријави, па пријавата се заклучува и се проверува дека е сè уште отворена и недоделена.
     99
     100{{{#!python
     101def assign_report(pool, admin_id, report_id, worker_id, priority, note, hold_seconds=0):
     102    with pool.connection() as conn:
     103        with conn.transaction():
     104            current = conn.execute(
     105                "SELECT status, priority FROM reports WHERE report_id = %s FOR UPDATE",
     106                (report_id,),
     107            ).fetchone()
     108            if hold_seconds:
     109                conn.execute("SELECT pg_sleep(%s)", (hold_seconds,))
     110            if current["status"] in ("resolved", "rejected"):
     111                raise AssignmentConflict("пријавата во меѓувреме е затворена")
     112            taken = conn.execute(
     113                """
     114                SELECT w.full_name FROM assignments a
     115                JOIN workers w ON w.worker_id = a.worker_id
     116                WHERE a.report_id = %s
     117                """,
     118                (report_id,),
     119            ).fetchone()
     120            if taken:
     121                raise AssignmentConflict(
     122                    f"пријавата во меѓувреме е доделена на {taken['full_name']}")
     123            if priority != current["priority"]:
     124                conn.execute("UPDATE reports SET priority = %s WHERE report_id = %s",
     125                             (priority, report_id))
     126            return conn.execute(
     127                """
     128                INSERT INTO assignments (note, admin_id, report_id, worker_id)
     129                VALUES (%s, %s, %s, %s)
     130                RETURNING assignment_id, assigned_at
     131                """,
     132                (note, admin_id, report_id, worker_id),
     133            ).fetchone()
     134}}}
     135
     136== Базен на конекции ==
     137
     138=== Поставување ===
     139
     140Се користи библиотеката psycopg_pool (инсталирана преку requirements.txt со psycopg[binary,pool]). Базенот се креира еднаш при стартување на апликацијата (cityfix/db.py) и се затвора при излез (cityfix/__main__.py). Параметрите се читаат од секцијата [pool] во config.ini:
     141
     142{{{
     143[pool]
     144min_size = 2
     145max_size = 10
     146timeout = 10
     147}}}
     148
     149{{{#!python
     150def create_pool():
     151    cfg = _config()
     152    db = cfg["database"]
     153    pool_cfg = cfg["pool"] if cfg.has_section("pool") else {}
     154    conninfo = (f"host={db.get('host', 'localhost')} port={db.get('port', '5432')} "
     155                f"dbname={db['dbname']} user={db['user']} password={db['password']} "
     156                "options='-c search_path=project'")
     157    pool = ConnectionPool(
     158        conninfo,
     159        min_size=int(pool_cfg.get("min_size", 2)),
     160        max_size=int(pool_cfg.get("max_size", 10)),
     161        timeout=float(pool_cfg.get("timeout", 10)),
     162        max_idle=300,
     163        kwargs={"row_factory": dict_row, "autocommit": True},
     164        check=ConnectionPool.check_connection,
     165        name="cityfix",
     166        open=False,
     167    )
     168    pool.open(wait=True)
     169    return pool
     170}}}
     171
     172 * min_size - број на конекции кои се отвораат при стартување и секогаш се чуваат отворени;
     173 * max_size - најголем број истовремено отворени конекции; при поголем број барања, базенот отвора дополнителни конекции до оваа граница;
     174 * timeout - најдолго време (во секунди) кое барањето чека на слободна конекција, пред да се пријави грешка;
     175 * max_idle - конекциите над min_size кои не се користат 5 минути се затвораат;
     176 * check - пред секое издавање, конекцијата се проверува, па прекинатите конекции (на пример по рестарт на серверот) автоматски се заменуваат;
     177 * kwargs - секоја конекција ги враќа редовите како речници, работи во autocommit режим, а search_path е поставен на шемата project.
     178
     179In main():
     180{{{#!python
     181pool = db.create_pool()
     182try:
     183    ...
     184    role[1](pool)
     185finally:
     186    pool.close()
     187}}}
     188
     189=== Користење на конекција од базенот ===
     190
     191Секоја операција зема конекција од базенот со with pool.connection() as conn: и ја враќа веднаш по завршувањето, дури и ако настане грешка. Со тоа ниту една конекција не е зафатена додека корисникот чита или внесува податоци на екранот.
     192
     193За едноставни прашалници се користат помошни функции во cityfix/db.py:
     194{{{#!python
     195def fetch_all(pool, sql, params=None):
     196    with pool.connection() as conn:
     197        return conn.execute(sql, params).fetchall()
     198}}}
     199
     200Пример од cityfix/admin.py (листа на работници од погледот v_worker_workload од P7):
     201{{{#!python
     202workers = db.fetch_all(pool, """
     203    SELECT worker_id, full_name, active_reports, active_high_priority
     204    FROM v_worker_workload
     205    ORDER BY active_reports, full_name
     206""")
     207}}}
     208
     209Пример од cityfix/citizen.py (проверка на слични пријави со функцијата од P7):
     210{{{#!python
     211similar = db.fetch_all(
     212    pool,
     213    "SELECT * FROM find_similar_reports(%s::integer, %s::numeric, %s::numeric, 150)",
     214    (category["category_id"], latitude, longitude),
     215)
     216}}}
     217
     218Трансакциите 1-4 погоре ја користат истата конекција од базенот за сите команди во трансакцијата.
     219
     220== Демонстрација на паралелна работа ==
     221
     222Модулот cityfix/demo_concurrency.py ги стартува истовремено повеќе нишки кои го користат истиот базен и истите функции како апликацијата. Се стартува со python -m cityfix.demo_concurrency (демонстрацијата ги менува податоците, па по неа треба повторно да се изврши data_load.sql). Резултат од извршувањето со max_size = 5:
     223
     224{{{
     2251) Базен на конекции: 20 истовремени барања
     226  [на почеток] отворени конекции: 2, слободни: 2, вкупно барања: 0, чекале на слободна конекција: 0
     227  20 барања завршија за 0.84 s
     228  [по барањата] отворени конекции: 5, слободни: 5, вкупно барања: 20, чекале на слободна конекција: 18
     229
     2302) Двајца администратори истовремено ја доделуваат иста пријава
     231  Администратор 1: одбиено - пријавата во меѓувреме е доделена на Горан Трајковски
     232  Администратор 2: успешно - внесено доделување бр. 73
     233
     2343) Двајца доделени работници истовремено го менуваат статусот на иста пријава
     235  Работник 1: одбиено - Пријавата во меѓувреме е променета во „Одбиена“. Обиди се повторно.
     236  Работник 2: успешно
     237}}}
     238
     239 * Во првиот дел, 20 барања од по 0,2 секунди се извршија за 0,84 секунди со само 5 конекции: базенот ги отвори дополнителните конекции до max_size, а останатите барања почекаа слободна конекција наместо да отвораат нови.
     240 * Во вториот и третиот дел, двете трансакции се обидуваат истовремено да ја сменат истата пријава. Втората трансакција чека на заклучувањето FOR UPDATE, потоа ја гледа променетата состојба и се откажува, без да внесе ништо. Која од двете трансакции ќе успее зависи од распоредот на нишките, но секогаш успева точно една.
     241
     242== Користење на вештачка интелигенција ==
     243
     244[wiki:AdvancedApplicationDevelopmentAIUsage Користење на вештачка интелигенција за напредното развивање на апликацијата]