xref: /plugin/combo/ComboStrap/Event.php (revision a6d63b89429f588dfa170d5ebcaef9d9de191156)
1<?php
2
3
4namespace ComboStrap;
5
6/**
7 * Class Event
8 * @package ComboStrap
9 * Asynchronous pub/sub system
10 *
11 * Dokuwiki allows event but they are synchronous
12 * because php does not live in multiple thread
13 *
14 * With the help of Sqlite, we make them asynchronous
15 */
16class Event
17{
18
19    const EVENT_TABLE_NAME = "EVENTS_QUEUE";
20
21    const CANONICAL = "support";
22
23    /**
24     * Uppercase mandatory (the column is uppercased when returnd from a *)
25     */
26    const EVENT_NAME_ATTRIBUTE = "NAME";
27
28    /**
29     * Uppercase mandatory (the column is uppercased when returnd from a *)
30     */
31    const EVENT_DATA_ATTRIBUTE = "DATA";
32    /**
33     * Uppercase mandatory (the column is uppercased when returnd from a *)
34     */
35    const TIMESTAMP_ATTRIBUTE = "TIMESTAMP";
36
37    /**
38     * process all replication request, created with {@link Event::createEvent()}
39     *
40     * by default, there is 5 pages in a default dokuwiki installation in the wiki namespace)
41     *
42     * @param int $maxEvent In case of a start or if there is a recursive bug. We don't want to take all the resources
43     *
44     */
45    public static function dispatchEvent(int $maxEvent = 10)
46    {
47
48        $lock = Lock::create("combo-event");
49        try {
50            $lock->acquire();
51        } catch (ExceptionTimeOut $e) {
52            // process running
53            return;
54        }
55
56        try {
57            try {
58                $sqlite = Sqlite::createOrGetBackendSqlite();
59            } catch (ExceptionSqliteNotAvailable $e) {
60                LogUtility::error("Sqlite is mandatory for asynchronous event", self::CANONICAL, $e);
61                return;
62            }
63
64
65            $rows = [];
66            /**
67             * Returning clause
68             * does not work
69             */
70            $version = $sqlite->getVersion();
71            if ($version > "3.35.0") {
72
73                // returning clause is available since 3.35 on delete
74                // https://www.sqlite.org/lang_returning.html
75
76                $eventTableName = self::EVENT_TABLE_NAME;
77                $statement = "delete from {$eventTableName} returning *";
78                // https://www.sqlite.org/lang_delete.html#optional_limit_and_order_by_clauses
79                if ($sqlite->hasOption("SQLITE_ENABLE_UPDATE_DELETE_LIMIT")) {
80                    $statement .= "order by timestamp limit {$maxEvent}";
81                }
82                $request = $sqlite->createRequest()
83                    ->setStatement($statement);
84                try {
85                    $rows = $request->execute()
86                        ->getRows();
87                    if (sizeof($rows) === 0) {
88                        return;
89                    }
90                } catch (ExceptionCompile $e) {
91                    LogUtility::error($e->getMessage(), $e->getCanonical(), $e);
92                } finally {
93                    $request->close();
94                }
95
96            }
97
98            /**
99             * Error in the block before or not the good version
100             * We try to get the records with a select/delete
101             */
102            if (sizeof($rows) === 0) {
103
104
105                // technically the lock system of dokuwiki does not allow two process to run on
106                // the indexer, we trust it
107                $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID];
108                $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes);
109                $select .= " order by " . self::TIMESTAMP_ATTRIBUTE . " limit {$maxEvent}";
110                $request = $sqlite->createRequest()
111                    ->setQuery($select);
112
113                $rowsSelected = [];
114                try {
115                    $rowsSelected = $request->execute()
116                        ->getRows();
117                    if (sizeof($rowsSelected) === 0) {
118                        return;
119                    }
120                } catch (ExceptionCompile $e) {
121                    LogUtility::msg("Error while retrieving the event {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical());
122                    return;
123                } finally {
124                    $request->close();
125                }
126
127                $eventTableName = self::EVENT_TABLE_NAME;
128                $rows = [];
129                foreach ($rowsSelected as $row) {
130                    $request = $sqlite->createRequest()
131                        ->setQueryParametrized("delete from $eventTableName where rowid = ? ", [$row[DatabasePageRow::ROWID]]);
132                    try {
133                        $changeCount = $request->execute()->getChangeCount();
134                        if ($changeCount !== 1) {
135                            LogUtility::msg("The delete of the event was not successful or it was deleted by another process", LogUtility::LVL_MSG_ERROR);
136                        } else {
137                            $rows[] = $row;
138                        }
139                    } catch (ExceptionCompile $e) {
140                        LogUtility::msg("Error while deleting the event. Message {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical());
141                        return;
142                    } finally {
143                        $request->close();
144                    }
145                }
146
147
148            }
149
150
151            $eventCounter = 0;
152            foreach ($rows as $row) {
153                $eventCounter++;
154                $eventName = $row[self::EVENT_NAME_ATTRIBUTE];
155                $eventData = [];
156                $eventDataJson = $row[self::EVENT_DATA_ATTRIBUTE];
157                if ($eventDataJson !== null) {
158                    try {
159                        $eventData = Json::createFromString($eventDataJson)->toArray();
160                    } catch (ExceptionCompile $e) {
161                        LogUtility::msg("The stored data for the event $eventName was not in the json format");
162                        continue;
163                    }
164                }
165                \dokuwiki\Extension\Event::createAndTrigger($eventName, $eventData);
166
167                if ($eventCounter >= $maxEvent) {
168                    break;
169                }
170
171            }
172        } finally {
173            $lock->release();
174        }
175
176    }
177
178    /**
179     * Ask a replication in the background
180     * @param string $name - a string with the reason
181     * @param array $data
182     */
183    public static
184    function createEvent(string $name, array $data)
185    {
186
187        try {
188            $sqlite = Sqlite::createOrGetBackendSqlite();
189        } catch (ExceptionSqliteNotAvailable $e) {
190            LogUtility::error("Unable to create the event $name. Sqlite is not available");
191            return;
192        }
193
194        /**
195         * If not present
196         */
197        $entry = array(
198            "name" => $name,
199            "timestamp" => Iso8601Date::createFromNow()->toString()
200        );
201
202
203        $entry["data"] = Json::createFromArray($data)->toPrettyJsonString();
204        $entry["data_hash"] = md5($entry["data"]);
205
206        /**
207         * Execute
208         */
209        $request = $sqlite->createRequest()
210            ->setTableRow(self::EVENT_TABLE_NAME, $entry);
211        try {
212            $request->execute();
213        } catch (ExceptionCompile $e) {
214            LogUtility::error("Unable to create the event $name. Error:" . $e->getMessage(), self::CANONICAL, $e);
215        } finally {
216            $request->close();
217        }
218
219
220    }
221
222    /**
223     * @param $pageId
224     *
225     * This is equivalent to {@link TaskRunner}
226     *
227     * lib/exe/taskrunner.php?id='.rawurlencode($ID)
228     * $taskRunner = new \dokuwiki\TaskRunner();
229     * $taskRunner->run();
230     *
231     */
232    public static function startTaskRunnerForPage($pageId)
233    {
234        $tmp = []; // No event data
235        $tmp['page'] = $pageId;
236        $evt = new \dokuwiki\Extension\Event('INDEXER_TASKS_RUN', $tmp);
237        $evt->advise_before();
238        $evt->advise_after();
239    }
240
241
242    public static function getQueue(string $eventName = null): array
243    {
244        try {
245            $sqlite = Sqlite::createOrGetBackendSqlite();
246        } catch (ExceptionSqliteNotAvailable $e) {
247            LogUtility::internalError("Sqlite is not available, no events was returned", self::CANONICAL);
248            return [];
249        }
250
251        /**
252         * Execute
253         */
254        $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID];
255        $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes);
256        $request = $sqlite->createRequest();
257        if (empty($eventName)) {
258            $request->setQuery($select);
259        } else {
260            $request->setQueryParametrized($select . " where " . self::EVENT_NAME_ATTRIBUTE . " = ?", [$eventName]);
261        }
262        try {
263            return $request->execute()
264                ->getRows();
265        } catch (ExceptionCompile $e) {
266            LogUtility::internalError("Unable to get the queue. Error:" . $e->getMessage(), self::CANONICAL, $e);
267            return [];
268        } finally {
269            $request->close();
270        }
271
272    }
273
274    /**
275     * @throws ExceptionCompile
276     */
277    public static function purgeQueue(): int
278    {
279        $sqlite = Sqlite::createOrGetBackendSqlite();
280        if ($sqlite === null) {
281            throw new ExceptionCompile("Sqlite is not available");
282        }
283
284
285        /**
286         * Execute
287         */
288        /** @noinspection SqlWithoutWhere */
289        $request = $sqlite->createRequest()
290            ->setQuery("delete from " . self::EVENT_TABLE_NAME);
291        try {
292            return $request->execute()
293                ->getChangeCount();
294        } catch (ExceptionCompile $e) {
295            throw new ExceptionCompile("Unable to count the number of event in the queue. Error:" . $e->getMessage(), self::CANONICAL, 0, $e);
296        } finally {
297            $request->close();
298        }
299    }
300
301    /**
302     * @throws ExceptionCompile
303     */
304    public static function getEvents(string $eventName): array
305    {
306        return Event::getQueue($eventName);
307    }
308
309
310}
311