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