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 = "event"; 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 = self::getLock(); 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 } catch (\Exception $e) { 173 LogUtility::internalError("An internal error has runned on event. " . $e->getMessage(), self::CANONICAL, $e); 174 $lock->release(); 175 } finally { 176 $lock->release(); 177 } 178 179 } 180 181 /** 182 * Ask a replication in the background 183 * @param string $name - a string with the reason 184 * @param array $data 185 */ 186 public static 187 function createEvent(string $name, array $data) 188 { 189 190 try { 191 $sqlite = Sqlite::createOrGetBackendSqlite(); 192 } catch (ExceptionSqliteNotAvailable $e) { 193 LogUtility::error("Unable to create the event $name. Sqlite is not available"); 194 return; 195 } 196 197 /** 198 * If not present 199 */ 200 $entry = array( 201 "name" => $name, 202 "timestamp" => Iso8601Date::createFromNow()->toString() 203 ); 204 205 206 $entry["data"] = Json::createFromArray($data)->toPrettyJsonString(); 207 $entry["data_hash"] = md5($entry["data"]); 208 209 /** 210 * Execute 211 */ 212 $request = $sqlite->createRequest() 213 ->setTableRow(self::EVENT_TABLE_NAME, $entry); 214 try { 215 $request->execute(); 216 } catch (ExceptionCompile $e) { 217 LogUtility::error("Unable to create the event $name. Error:" . $e->getMessage(), self::CANONICAL, $e); 218 } finally { 219 $request->close(); 220 } 221 222 223 } 224 225 /** 226 * @param $pageId 227 * 228 * This is equivalent to {@link TaskRunner} 229 * 230 * lib/exe/taskrunner.php?id='.rawurlencode($ID) 231 * $taskRunner = new \dokuwiki\TaskRunner(); 232 * $taskRunner->run(); 233 * 234 */ 235 public static function startTaskRunnerForPage($pageId) 236 { 237 $tmp = []; // No event data 238 $tmp['page'] = $pageId; 239 $evt = new \dokuwiki\Extension\Event('INDEXER_TASKS_RUN', $tmp); 240 $evt->advise_before(); 241 $evt->advise_after(); 242 } 243 244 245 public static function getQueue(string $eventName = null): array 246 { 247 try { 248 $sqlite = Sqlite::createOrGetBackendSqlite(); 249 } catch (ExceptionSqliteNotAvailable $e) { 250 LogUtility::internalError("Sqlite is not available, no events was returned", self::CANONICAL); 251 return []; 252 } 253 254 /** 255 * Execute 256 */ 257 $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID]; 258 $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes); 259 $request = $sqlite->createRequest(); 260 if (empty($eventName)) { 261 $request->setQuery($select); 262 } else { 263 $request->setQueryParametrized($select . " where " . self::EVENT_NAME_ATTRIBUTE . " = ?", [$eventName]); 264 } 265 try { 266 return $request->execute() 267 ->getRows(); 268 } catch (ExceptionCompile $e) { 269 LogUtility::internalError("Unable to get the queue. Error:" . $e->getMessage(), self::CANONICAL, $e); 270 return []; 271 } finally { 272 $request->close(); 273 } 274 275 } 276 277 /** 278 * @throws ExceptionCompile 279 */ 280 public static function purgeQueue(): int 281 { 282 $sqlite = Sqlite::createOrGetBackendSqlite(); 283 if ($sqlite === null) { 284 throw new ExceptionCompile("Sqlite is not available"); 285 } 286 287 288 /** 289 * Execute 290 */ 291 /** @noinspection SqlWithoutWhere */ 292 $request = $sqlite->createRequest() 293 ->setQuery("delete from " . self::EVENT_TABLE_NAME); 294 try { 295 return $request->execute() 296 ->getChangeCount(); 297 } catch (ExceptionCompile $e) { 298 throw new ExceptionCompile("Unable to count the number of event in the queue. Error:" . $e->getMessage(), self::CANONICAL, 0, $e); 299 } finally { 300 $request->close(); 301 } 302 } 303 304 /** 305 * @throws ExceptionCompile 306 */ 307 public static function getEvents(string $eventName): array 308 { 309 return Event::getQueue($eventName); 310 } 311 312 public static function getLock(): Lock 313 { 314 return Lock::create("combo-event"); 315 } 316 317 318} 319