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 = 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 } 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 public static function getLock(): Lock 310 { 311 return Lock::create("combo-event"); 312 } 313 314 315} 316