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