UIDGenerator.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <?php
  2. /**
  3. * This file deals with UID generation.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use Wikimedia\Assert\Assert;
  23. use MediaWiki\MediaWikiServices;
  24. /**
  25. * Class for getting statistically unique IDs
  26. *
  27. * @since 1.21
  28. */
  29. class UIDGenerator {
  30. /** @var UIDGenerator */
  31. protected static $instance = null;
  32. protected $nodeIdFile; // string; local file path
  33. protected $nodeId32; // string; node ID in binary (32 bits)
  34. protected $nodeId48; // string; node ID in binary (48 bits)
  35. protected $lockFile88; // string; local file path
  36. protected $lockFile128; // string; local file path
  37. protected $lockFileUUID; // string; local file path
  38. /** @var array */
  39. protected $fileHandles = []; // cache file handles
  40. const QUICK_RAND = 1; // get randomness from fast and insecure sources
  41. const QUICK_VOLATILE = 2; // use an APC like in-memory counter if available
  42. protected function __construct() {
  43. $this->nodeIdFile = wfTempDir() . '/mw-' . __CLASS__ . '-UID-nodeid';
  44. $nodeId = '';
  45. if ( is_file( $this->nodeIdFile ) ) {
  46. $nodeId = file_get_contents( $this->nodeIdFile );
  47. }
  48. // Try to get some ID that uniquely identifies this machine (RFC 4122)...
  49. if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
  50. Wikimedia\suppressWarnings();
  51. if ( wfIsWindows() ) {
  52. // https://technet.microsoft.com/en-us/library/bb490913.aspx
  53. $csv = trim( wfShellExec( 'getmac /NH /FO CSV' ) );
  54. $line = substr( $csv, 0, strcspn( $csv, "\n" ) );
  55. $info = str_getcsv( $line );
  56. $nodeId = isset( $info[0] ) ? str_replace( '-', '', $info[0] ) : '';
  57. } elseif ( is_executable( '/sbin/ifconfig' ) ) { // Linux/BSD/Solaris/OS X
  58. // See https://linux.die.net/man/8/ifconfig
  59. $m = [];
  60. preg_match( '/\s([0-9a-f]{2}(:[0-9a-f]{2}){5})\s/',
  61. wfShellExec( '/sbin/ifconfig -a' ), $m );
  62. $nodeId = isset( $m[1] ) ? str_replace( ':', '', $m[1] ) : '';
  63. }
  64. Wikimedia\restoreWarnings();
  65. if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
  66. $nodeId = MWCryptRand::generateHex( 12, true );
  67. $nodeId[1] = dechex( hexdec( $nodeId[1] ) | 0x1 ); // set multicast bit
  68. }
  69. file_put_contents( $this->nodeIdFile, $nodeId ); // cache
  70. }
  71. $this->nodeId32 = Wikimedia\base_convert( substr( sha1( $nodeId ), 0, 8 ), 16, 2, 32 );
  72. $this->nodeId48 = Wikimedia\base_convert( $nodeId, 16, 2, 48 );
  73. // If different processes run as different users, they may have different temp dirs.
  74. // This is dealt with by initializing the clock sequence number and counters randomly.
  75. $this->lockFile88 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-88';
  76. $this->lockFile128 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-128';
  77. $this->lockFileUUID = wfTempDir() . '/mw-' . __CLASS__ . '-UUID-128';
  78. }
  79. /**
  80. * @todo move to MW-specific factory class and inject temp dir
  81. * @return UIDGenerator
  82. */
  83. protected static function singleton() {
  84. if ( self::$instance === null ) {
  85. self::$instance = new self();
  86. }
  87. return self::$instance;
  88. }
  89. /**
  90. * Get a statistically unique 88-bit unsigned integer ID string.
  91. * The bits of the UID are prefixed with the time (down to the millisecond).
  92. *
  93. * These IDs are suitable as values for the shard key of distributed data.
  94. * If a column uses these as values, it should be declared UNIQUE to handle collisions.
  95. * New rows almost always have higher UIDs, which makes B-TREE updates on INSERT fast.
  96. * They can also be stored "DECIMAL(27) UNSIGNED" or BINARY(11) in MySQL.
  97. *
  98. * UID generation is serialized on each server (as the node ID is for the whole machine).
  99. *
  100. * @param int $base Specifies a base other than 10
  101. * @return string Number
  102. * @throws RuntimeException
  103. */
  104. public static function newTimestampedUID88( $base = 10 ) {
  105. Assert::parameterType( 'integer', $base, '$base' );
  106. Assert::parameter( $base <= 36, '$base', 'must be <= 36' );
  107. Assert::parameter( $base >= 2, '$base', 'must be >= 2' );
  108. $gen = self::singleton();
  109. $info = $gen->getTimeAndDelay( 'lockFile88', 1, 1024, 1024 );
  110. $info['offsetCounter'] = $info['offsetCounter'] % 1024;
  111. return Wikimedia\base_convert( $gen->getTimestampedID88( $info ), 2, $base );
  112. }
  113. /**
  114. * @param array $info The result of UIDGenerator::getTimeAndDelay() or
  115. * a plain (UIDGenerator::millitime(), counter, clock sequence) array.
  116. * @return string 88 bits
  117. * @throws RuntimeException
  118. */
  119. protected function getTimestampedID88( array $info ) {
  120. if ( isset( $info['time'] ) ) {
  121. $time = $info['time'];
  122. $counter = $info['offsetCounter'];
  123. } else {
  124. $time = $info[0];
  125. $counter = $info[1];
  126. }
  127. // Take the 46 LSBs of "milliseconds since epoch"
  128. $id_bin = $this->millisecondsSinceEpochBinary( $time );
  129. // Add a 10 bit counter resulting in 56 bits total
  130. $id_bin .= str_pad( decbin( $counter ), 10, '0', STR_PAD_LEFT );
  131. // Add the 32 bit node ID resulting in 88 bits total
  132. $id_bin .= $this->nodeId32;
  133. // Convert to a 1-27 digit integer string
  134. if ( strlen( $id_bin ) !== 88 ) {
  135. throw new RuntimeException( "Detected overflow for millisecond timestamp." );
  136. }
  137. return $id_bin;
  138. }
  139. /**
  140. * Get a statistically unique 128-bit unsigned integer ID string.
  141. * The bits of the UID are prefixed with the time (down to the millisecond).
  142. *
  143. * These IDs are suitable as globally unique IDs, without any enforced uniqueness.
  144. * New rows almost always have higher UIDs, which makes B-TREE updates on INSERT fast.
  145. * They can also be stored as "DECIMAL(39) UNSIGNED" or BINARY(16) in MySQL.
  146. *
  147. * UID generation is serialized on each server (as the node ID is for the whole machine).
  148. *
  149. * @param int $base Specifies a base other than 10
  150. * @return string Number
  151. * @throws RuntimeException
  152. */
  153. public static function newTimestampedUID128( $base = 10 ) {
  154. Assert::parameterType( 'integer', $base, '$base' );
  155. Assert::parameter( $base <= 36, '$base', 'must be <= 36' );
  156. Assert::parameter( $base >= 2, '$base', 'must be >= 2' );
  157. $gen = self::singleton();
  158. $info = $gen->getTimeAndDelay( 'lockFile128', 16384, 1048576, 1048576 );
  159. $info['offsetCounter'] = $info['offsetCounter'] % 1048576;
  160. return Wikimedia\base_convert( $gen->getTimestampedID128( $info ), 2, $base );
  161. }
  162. /**
  163. * @param array $info The result of UIDGenerator::getTimeAndDelay() or
  164. * a plain (UIDGenerator::millitime(), counter, clock sequence) array.
  165. * @return string 128 bits
  166. * @throws RuntimeException
  167. */
  168. protected function getTimestampedID128( array $info ) {
  169. if ( isset( $info['time'] ) ) {
  170. $time = $info['time'];
  171. $counter = $info['offsetCounter'];
  172. $clkSeq = $info['clkSeq'];
  173. } else {
  174. $time = $info[0];
  175. $counter = $info[1];
  176. $clkSeq = $info[2];
  177. }
  178. // Take the 46 LSBs of "milliseconds since epoch"
  179. $id_bin = $this->millisecondsSinceEpochBinary( $time );
  180. // Add a 20 bit counter resulting in 66 bits total
  181. $id_bin .= str_pad( decbin( $counter ), 20, '0', STR_PAD_LEFT );
  182. // Add a 14 bit clock sequence number resulting in 80 bits total
  183. $id_bin .= str_pad( decbin( $clkSeq ), 14, '0', STR_PAD_LEFT );
  184. // Add the 48 bit node ID resulting in 128 bits total
  185. $id_bin .= $this->nodeId48;
  186. // Convert to a 1-39 digit integer string
  187. if ( strlen( $id_bin ) !== 128 ) {
  188. throw new RuntimeException( "Detected overflow for millisecond timestamp." );
  189. }
  190. return $id_bin;
  191. }
  192. /**
  193. * Return an RFC4122 compliant v1 UUID
  194. *
  195. * @return string
  196. * @throws RuntimeException
  197. * @since 1.27
  198. */
  199. public static function newUUIDv1() {
  200. $gen = self::singleton();
  201. // There can be up to 10000 intervals for the same millisecond timestamp.
  202. // [0,4999] counter + [0,5000] offset is in [0,9999] for the offset counter.
  203. // Add this onto the timestamp to allow making up to 5000 IDs per second.
  204. return $gen->getUUIDv1( $gen->getTimeAndDelay( 'lockFileUUID', 16384, 5000, 5001 ) );
  205. }
  206. /**
  207. * Return an RFC4122 compliant v1 UUID
  208. *
  209. * @return string 32 hex characters with no hyphens
  210. * @throws RuntimeException
  211. * @since 1.27
  212. */
  213. public static function newRawUUIDv1() {
  214. return str_replace( '-', '', self::newUUIDv1() );
  215. }
  216. /**
  217. * @param array $info Result of UIDGenerator::getTimeAndDelay()
  218. * @return string 128 bits
  219. */
  220. protected function getUUIDv1( array $info ) {
  221. $clkSeq_bin = Wikimedia\base_convert( $info['clkSeq'], 10, 2, 14 );
  222. $time_bin = $this->intervalsSinceGregorianBinary( $info['time'], $info['offsetCounter'] );
  223. // Take the 32 bits of "time low"
  224. $id_bin = substr( $time_bin, 28, 32 );
  225. // Add 16 bits of "time mid" resulting in 48 bits total
  226. $id_bin .= substr( $time_bin, 12, 16 );
  227. // Add 4 bit version resulting in 52 bits total
  228. $id_bin .= '0001';
  229. // Add 12 bits of "time high" resulting in 64 bits total
  230. $id_bin .= substr( $time_bin, 0, 12 );
  231. // Add 2 bits of "variant" resulting in 66 bits total
  232. $id_bin .= '10';
  233. // Add 6 bits of "clock seq high" resulting in 72 bits total
  234. $id_bin .= substr( $clkSeq_bin, 0, 6 );
  235. // Add 8 bits of "clock seq low" resulting in 80 bits total
  236. $id_bin .= substr( $clkSeq_bin, 6, 8 );
  237. // Add the 48 bit node ID resulting in 128 bits total
  238. $id_bin .= $this->nodeId48;
  239. // Convert to a 32 char hex string with dashes
  240. if ( strlen( $id_bin ) !== 128 ) {
  241. throw new RuntimeException( "Detected overflow for millisecond timestamp." );
  242. }
  243. $hex = Wikimedia\base_convert( $id_bin, 2, 16, 32 );
  244. return sprintf( '%s-%s-%s-%s-%s',
  245. // "time_low" (32 bits)
  246. substr( $hex, 0, 8 ),
  247. // "time_mid" (16 bits)
  248. substr( $hex, 8, 4 ),
  249. // "time_hi_and_version" (16 bits)
  250. substr( $hex, 12, 4 ),
  251. // "clk_seq_hi_res" (8 bits) and "clk_seq_low" (8 bits)
  252. substr( $hex, 16, 4 ),
  253. // "node" (48 bits)
  254. substr( $hex, 20, 12 )
  255. );
  256. }
  257. /**
  258. * Return an RFC4122 compliant v4 UUID
  259. *
  260. * @param int $flags Bitfield (supports UIDGenerator::QUICK_RAND)
  261. * @return string
  262. * @throws RuntimeException
  263. */
  264. public static function newUUIDv4( $flags = 0 ) {
  265. $hex = ( $flags & self::QUICK_RAND )
  266. ? wfRandomString( 31 )
  267. : MWCryptRand::generateHex( 31 );
  268. return sprintf( '%s-%s-%s-%s-%s',
  269. // "time_low" (32 bits)
  270. substr( $hex, 0, 8 ),
  271. // "time_mid" (16 bits)
  272. substr( $hex, 8, 4 ),
  273. // "time_hi_and_version" (16 bits)
  274. '4' . substr( $hex, 12, 3 ),
  275. // "clk_seq_hi_res" (8 bits, variant is binary 10x) and "clk_seq_low" (8 bits)
  276. dechex( 0x8 | ( hexdec( $hex[15] ) & 0x3 ) ) . $hex[16] . substr( $hex, 17, 2 ),
  277. // "node" (48 bits)
  278. substr( $hex, 19, 12 )
  279. );
  280. }
  281. /**
  282. * Return an RFC4122 compliant v4 UUID
  283. *
  284. * @param int $flags Bitfield (supports UIDGenerator::QUICK_RAND)
  285. * @return string 32 hex characters with no hyphens
  286. * @throws RuntimeException
  287. */
  288. public static function newRawUUIDv4( $flags = 0 ) {
  289. return str_replace( '-', '', self::newUUIDv4( $flags ) );
  290. }
  291. /**
  292. * Return an ID that is sequential *only* for this node and bucket
  293. *
  294. * These IDs are suitable for per-host sequence numbers, e.g. for some packet protocols.
  295. * If UIDGenerator::QUICK_VOLATILE is used the counter might reset on server restart.
  296. *
  297. * @param string $bucket Arbitrary bucket name (should be ASCII)
  298. * @param int $bits Bit size (<=48) of resulting numbers before wrap-around
  299. * @param int $flags (supports UIDGenerator::QUICK_VOLATILE)
  300. * @return float Integer value as float
  301. * @since 1.23
  302. */
  303. public static function newSequentialPerNodeID( $bucket, $bits = 48, $flags = 0 ) {
  304. return current( self::newSequentialPerNodeIDs( $bucket, $bits, 1, $flags ) );
  305. }
  306. /**
  307. * Return IDs that are sequential *only* for this node and bucket
  308. *
  309. * @see UIDGenerator::newSequentialPerNodeID()
  310. * @param string $bucket Arbitrary bucket name (should be ASCII)
  311. * @param int $bits Bit size (16 to 48) of resulting numbers before wrap-around
  312. * @param int $count Number of IDs to return
  313. * @param int $flags (supports UIDGenerator::QUICK_VOLATILE)
  314. * @return array Ordered list of float integer values
  315. * @since 1.23
  316. */
  317. public static function newSequentialPerNodeIDs( $bucket, $bits, $count, $flags = 0 ) {
  318. $gen = self::singleton();
  319. return $gen->getSequentialPerNodeIDs( $bucket, $bits, $count, $flags );
  320. }
  321. /**
  322. * Return IDs that are sequential *only* for this node and bucket
  323. *
  324. * @see UIDGenerator::newSequentialPerNodeID()
  325. * @param string $bucket Arbitrary bucket name (should be ASCII)
  326. * @param int $bits Bit size (16 to 48) of resulting numbers before wrap-around
  327. * @param int $count Number of IDs to return
  328. * @param int $flags (supports UIDGenerator::QUICK_VOLATILE)
  329. * @return array Ordered list of float integer values
  330. * @throws RuntimeException
  331. */
  332. protected function getSequentialPerNodeIDs( $bucket, $bits, $count, $flags ) {
  333. if ( $count <= 0 ) {
  334. return []; // nothing to do
  335. } elseif ( $bits < 16 || $bits > 48 ) {
  336. throw new RuntimeException( "Requested bit size ($bits) is out of range." );
  337. }
  338. $counter = null; // post-increment persistent counter value
  339. // Use APC/etc if requested, available, and not in CLI mode;
  340. // Counter values would not survive across script instances in CLI mode.
  341. $cache = null;
  342. if ( ( $flags & self::QUICK_VOLATILE ) && !wfIsCLI() ) {
  343. $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
  344. }
  345. if ( $cache ) {
  346. $counter = $cache->incrWithInit( $bucket, $cache::TTL_INDEFINITE, $count, $count );
  347. if ( $counter === false ) {
  348. throw new RuntimeException( 'Unable to set value to ' . get_class( $cache ) );
  349. }
  350. }
  351. // Note: use of fmod() avoids "division by zero" on 32 bit machines
  352. if ( $counter === null ) {
  353. $path = wfTempDir() . '/mw-' . __CLASS__ . '-' . rawurlencode( $bucket ) . '-48';
  354. // Get the UID lock file handle
  355. if ( isset( $this->fileHandles[$path] ) ) {
  356. $handle = $this->fileHandles[$path];
  357. } else {
  358. $handle = fopen( $path, 'cb+' );
  359. $this->fileHandles[$path] = $handle ?: null; // cache
  360. }
  361. // Acquire the UID lock file
  362. if ( $handle === false ) {
  363. throw new RuntimeException( "Could not open '{$path}'." );
  364. } elseif ( !flock( $handle, LOCK_EX ) ) {
  365. fclose( $handle );
  366. throw new RuntimeException( "Could not acquire '{$path}'." );
  367. }
  368. // Fetch the counter value and increment it...
  369. rewind( $handle );
  370. $counter = floor( trim( fgets( $handle ) ) ) + $count; // fetch as float
  371. // Write back the new counter value
  372. ftruncate( $handle, 0 );
  373. rewind( $handle );
  374. fwrite( $handle, fmod( $counter, 2 ** 48 ) ); // warp-around as needed
  375. fflush( $handle );
  376. // Release the UID lock file
  377. flock( $handle, LOCK_UN );
  378. }
  379. $ids = [];
  380. $divisor = 2 ** $bits;
  381. $currentId = floor( $counter - $count ); // pre-increment counter value
  382. for ( $i = 0; $i < $count; ++$i ) {
  383. $ids[] = fmod( ++$currentId, $divisor );
  384. }
  385. return $ids;
  386. }
  387. /**
  388. * Get a (time,counter,clock sequence) where (time,counter) is higher
  389. * than any previous (time,counter) value for the given clock sequence.
  390. * This is useful for making UIDs sequential on a per-node bases.
  391. *
  392. * @param string $lockFile Name of a local lock file
  393. * @param int $clockSeqSize The number of possible clock sequence values
  394. * @param int $counterSize The number of possible counter values
  395. * @param int $offsetSize The number of possible offset values
  396. * @return array (result of UIDGenerator::millitime(), counter, clock sequence)
  397. * @throws RuntimeException
  398. */
  399. protected function getTimeAndDelay( $lockFile, $clockSeqSize, $counterSize, $offsetSize ) {
  400. // Get the UID lock file handle
  401. if ( isset( $this->fileHandles[$lockFile] ) ) {
  402. $handle = $this->fileHandles[$lockFile];
  403. } else {
  404. $handle = fopen( $this->$lockFile, 'cb+' );
  405. $this->fileHandles[$lockFile] = $handle ?: null; // cache
  406. }
  407. // Acquire the UID lock file
  408. if ( $handle === false ) {
  409. throw new RuntimeException( "Could not open '{$this->$lockFile}'." );
  410. } elseif ( !flock( $handle, LOCK_EX ) ) {
  411. fclose( $handle );
  412. throw new RuntimeException( "Could not acquire '{$this->$lockFile}'." );
  413. }
  414. // Get the current timestamp, clock sequence number, last time, and counter
  415. rewind( $handle );
  416. $data = explode( ' ', fgets( $handle ) ); // "<clk seq> <sec> <msec> <counter> <offset>"
  417. $clockChanged = false; // clock set back significantly?
  418. if ( count( $data ) == 5 ) { // last UID info already initialized
  419. $clkSeq = (int)$data[0] % $clockSeqSize;
  420. $prevTime = [ (int)$data[1], (int)$data[2] ];
  421. $offset = (int)$data[4] % $counterSize; // random counter offset
  422. $counter = 0; // counter for UIDs with the same timestamp
  423. // Delay until the clock reaches the time of the last ID.
  424. // This detects any microtime() drift among processes.
  425. $time = $this->timeWaitUntil( $prevTime );
  426. if ( !$time ) { // too long to delay?
  427. $clockChanged = true; // bump clock sequence number
  428. $time = self::millitime();
  429. } elseif ( $time == $prevTime ) {
  430. // Bump the counter if there are timestamp collisions
  431. $counter = (int)$data[3] % $counterSize;
  432. if ( ++$counter >= $counterSize ) { // sanity (starts at 0)
  433. flock( $handle, LOCK_UN ); // abort
  434. throw new RuntimeException( "Counter overflow for timestamp value." );
  435. }
  436. }
  437. } else { // last UID info not initialized
  438. $clkSeq = mt_rand( 0, $clockSeqSize - 1 );
  439. $counter = 0;
  440. $offset = mt_rand( 0, $offsetSize - 1 );
  441. $time = self::millitime();
  442. }
  443. // microtime() and gettimeofday() can drift from time() at least on Windows.
  444. // The drift is immediate for processes running while the system clock changes.
  445. // time() does not have this problem. See https://bugs.php.net/bug.php?id=42659.
  446. if ( abs( time() - $time[0] ) >= 2 ) {
  447. // We don't want processes using too high or low timestamps to avoid duplicate
  448. // UIDs and clock sequence number churn. This process should just be restarted.
  449. flock( $handle, LOCK_UN ); // abort
  450. throw new RuntimeException( "Process clock is outdated or drifted." );
  451. }
  452. // If microtime() is synced and a clock change was detected, then the clock went back
  453. if ( $clockChanged ) {
  454. // Bump the clock sequence number and also randomize the counter offset,
  455. // which is useful for UIDs that do not include the clock sequence number.
  456. $clkSeq = ( $clkSeq + 1 ) % $clockSeqSize;
  457. $offset = mt_rand( 0, $offsetSize - 1 );
  458. trigger_error( "Clock was set back; sequence number incremented." );
  459. }
  460. // Update the (clock sequence number, timestamp, counter)
  461. ftruncate( $handle, 0 );
  462. rewind( $handle );
  463. fwrite( $handle, "{$clkSeq} {$time[0]} {$time[1]} {$counter} {$offset}" );
  464. fflush( $handle );
  465. // Release the UID lock file
  466. flock( $handle, LOCK_UN );
  467. return [
  468. 'time' => $time,
  469. 'counter' => $counter,
  470. 'clkSeq' => $clkSeq,
  471. 'offset' => $offset,
  472. 'offsetCounter' => $counter + $offset
  473. ];
  474. }
  475. /**
  476. * Wait till the current timestamp reaches $time and return the current
  477. * timestamp. This returns false if it would have to wait more than 10ms.
  478. *
  479. * @param array $time Result of UIDGenerator::millitime()
  480. * @return array|bool UIDGenerator::millitime() result or false
  481. */
  482. protected function timeWaitUntil( array $time ) {
  483. do {
  484. $ct = self::millitime();
  485. if ( $ct >= $time ) { // https://secure.php.net/manual/en/language.operators.comparison.php
  486. return $ct; // current timestamp is higher than $time
  487. }
  488. } while ( ( ( $time[0] - $ct[0] ) * 1000 + ( $time[1] - $ct[1] ) ) <= 10 );
  489. return false;
  490. }
  491. /**
  492. * @param array $time Result of UIDGenerator::millitime()
  493. * @return string 46 LSBs of "milliseconds since epoch" in binary (rolls over in 4201)
  494. * @throws RuntimeException
  495. */
  496. protected function millisecondsSinceEpochBinary( array $time ) {
  497. list( $sec, $msec ) = $time;
  498. $ts = 1000 * $sec + $msec;
  499. if ( $ts > 2 ** 52 ) {
  500. throw new RuntimeException( __METHOD__ .
  501. ': sorry, this function doesn\'t work after the year 144680' );
  502. }
  503. return substr( Wikimedia\base_convert( $ts, 10, 2, 46 ), -46 );
  504. }
  505. /**
  506. * @param array $time Result of UIDGenerator::millitime()
  507. * @param int $delta Number of intervals to add on to the timestamp
  508. * @return string 60 bits of "100ns intervals since 15 October 1582" (rolls over in 3400)
  509. * @throws RuntimeException
  510. */
  511. protected function intervalsSinceGregorianBinary( array $time, $delta = 0 ) {
  512. list( $sec, $msec ) = $time;
  513. $offset = '122192928000000000';
  514. if ( PHP_INT_SIZE >= 8 ) { // 64 bit integers
  515. $ts = ( 1000 * $sec + $msec ) * 10000 + (int)$offset + $delta;
  516. $id_bin = str_pad( decbin( $ts % ( 2 ** 60 ) ), 60, '0', STR_PAD_LEFT );
  517. } elseif ( extension_loaded( 'gmp' ) ) {
  518. $ts = gmp_add( gmp_mul( (string)$sec, '1000' ), (string)$msec ); // ms
  519. $ts = gmp_add( gmp_mul( $ts, '10000' ), $offset ); // 100ns intervals
  520. $ts = gmp_add( $ts, (string)$delta );
  521. $ts = gmp_mod( $ts, gmp_pow( '2', '60' ) ); // wrap around
  522. $id_bin = str_pad( gmp_strval( $ts, 2 ), 60, '0', STR_PAD_LEFT );
  523. } elseif ( extension_loaded( 'bcmath' ) ) {
  524. $ts = bcadd( bcmul( $sec, 1000 ), $msec ); // ms
  525. $ts = bcadd( bcmul( $ts, 10000 ), $offset ); // 100ns intervals
  526. $ts = bcadd( $ts, $delta );
  527. $ts = bcmod( $ts, bcpow( 2, 60 ) ); // wrap around
  528. $id_bin = Wikimedia\base_convert( $ts, 10, 2, 60 );
  529. } else {
  530. throw new RuntimeException( 'bcmath or gmp extension required for 32 bit machines.' );
  531. }
  532. return $id_bin;
  533. }
  534. /**
  535. * @return array (current time in seconds, milliseconds since then)
  536. */
  537. protected static function millitime() {
  538. list( $msec, $sec ) = explode( ' ', microtime() );
  539. return [ (int)$sec, (int)( $msec * 1000 ) ];
  540. }
  541. /**
  542. * Delete all cache files that have been created.
  543. *
  544. * This is a cleanup method primarily meant to be used from unit tests to
  545. * avoid poluting the local filesystem. If used outside of a unit test
  546. * environment it should be used with caution as it may destroy state saved
  547. * in the files.
  548. *
  549. * @see unitTestTearDown
  550. * @since 1.23
  551. */
  552. protected function deleteCacheFiles() {
  553. // Bug: 44850
  554. foreach ( $this->fileHandles as $path => $handle ) {
  555. if ( $handle !== null ) {
  556. fclose( $handle );
  557. }
  558. if ( is_file( $path ) ) {
  559. unlink( $path );
  560. }
  561. unset( $this->fileHandles[$path] );
  562. }
  563. if ( is_file( $this->nodeIdFile ) ) {
  564. unlink( $this->nodeIdFile );
  565. }
  566. }
  567. /**
  568. * Cleanup resources when tearing down after a unit test.
  569. *
  570. * This is a cleanup method primarily meant to be used from unit tests to
  571. * avoid poluting the local filesystem. If used outside of a unit test
  572. * environment it should be used with caution as it may destroy state saved
  573. * in the files.
  574. *
  575. * @see deleteCacheFiles
  576. * @since 1.23
  577. */
  578. public static function unitTestTearDown() {
  579. // Bug: 44850
  580. $gen = self::singleton();
  581. $gen->deleteCacheFiles();
  582. }
  583. function __destruct() {
  584. array_map( 'fclose', array_filter( $this->fileHandles ) );
  585. }
  586. }