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