From 21750f8d6806d1d228be8453313f466c2b6fd420 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sat, 23 May 2020 03:49:20 +0300 Subject: [PATCH 01/25] Allow downloadToResponse for media without size --- src/danog/MadelineProto/MTProtoTools/Files.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/danog/MadelineProto/MTProtoTools/Files.php b/src/danog/MadelineProto/MTProtoTools/Files.php index 70b9106c..a64d89fa 100644 --- a/src/danog/MadelineProto/MTProtoTools/Files.php +++ b/src/danog/MadelineProto/MTProtoTools/Files.php @@ -645,7 +645,7 @@ trait Files */ public function getPropicInfo($data): \Generator { - return yield from $this->getDownloadInfo($this->chats[(yield from $this->getInfo($data))['bot_api_id']]); + return yield from $this->getDownloadInfo(yield $this->chats[(yield from $this->getInfo($data))['bot_api_id']]); } /** * Extract file info from bot API message. From ff7d93f52d001b04fb90ee4431da75156de2932b Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sat, 25 Apr 2020 20:36:15 +0300 Subject: [PATCH 02/25] phpDoc fix --- src/danog/MadelineProto/API.php | 4 ++-- src/danog/MadelineProto/MTProto.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/danog/MadelineProto/API.php b/src/danog/MadelineProto/API.php index 0c1b7242..aa352bd6 100644 --- a/src/danog/MadelineProto/API.php +++ b/src/danog/MadelineProto/API.php @@ -41,7 +41,7 @@ class API extends InternalDoc /** * Instance of MadelineProto. * - * @var ?MTProto + * @var null|MTProto */ public $API; @@ -66,7 +66,7 @@ class API extends InternalDoc * * @internal * - * @var ?MyTelegramOrgWrapper + * @var null|MyTelegramOrgWrapper */ private $myTelegramOrgWrapper; diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 4e7eb79d..3a17eda4 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -206,7 +206,7 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Instance of wrapper API. * - * @var ?APIWrapper + * @var null|APIWrapper */ public $wrapper; /** From ed493330a65ee1d0e956295f3d8830d5764c6fb8 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sat, 25 Apr 2020 22:57:55 +0300 Subject: [PATCH 03/25] Database properties types --- composer.json | 1 + src/danog/MadelineProto/Db/DbArray.php | 11 + .../MadelineProto/Db/DbPropertiesFabric.php | 49 ++ src/danog/MadelineProto/Db/DbType.php | 8 + src/danog/MadelineProto/Db/MemoryArray.php | 15 + src/danog/MadelineProto/Db/Mysql.php | 45 ++ src/danog/MadelineProto/Db/MysqlArray.php | 429 ++++++++++++++++++ .../MadelineProto/Db/SharedMemoryArray.php | 25 + src/danog/MadelineProto/MTProto.php | 80 +++- 9 files changed, 650 insertions(+), 13 deletions(-) create mode 100644 src/danog/MadelineProto/Db/DbArray.php create mode 100644 src/danog/MadelineProto/Db/DbPropertiesFabric.php create mode 100644 src/danog/MadelineProto/Db/DbType.php create mode 100644 src/danog/MadelineProto/Db/MemoryArray.php create mode 100644 src/danog/MadelineProto/Db/Mysql.php create mode 100644 src/danog/MadelineProto/Db/MysqlArray.php create mode 100644 src/danog/MadelineProto/Db/SharedMemoryArray.php diff --git a/composer.json b/composer.json index 3e5debca..0555de72 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "amphp/dns": "^1", "amphp/byte-stream": "^1", "amphp/file": "^1", + "amphp/mysql": "^2.0", "danog/dns-over-https": "^0.2", "amphp/http-client-cookies": "^1", "danog/tg-file-decoder": "^0.1", diff --git a/src/danog/MadelineProto/Db/DbArray.php b/src/danog/MadelineProto/Db/DbArray.php new file mode 100644 index 00000000..263d9a93 --- /dev/null +++ b/src/danog/MadelineProto/Db/DbArray.php @@ -0,0 +1,11 @@ +getArrayCopy(); + } + return new static($value); + } +} \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php new file mode 100644 index 00000000..cada90af --- /dev/null +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -0,0 +1,45 @@ + $this->table, + 'key' => $this->key, + 'settings' => $this->settings + ]; + } + + public function __unserialize($data): void + { + foreach ($data as $property => $value) { + $this->{$property} = $value; + } + $this->initDbConnection(); + } + + public static function getInstance(array $settings, string $name, $value = []): DbType + { + $instance = new static(); + $instance->table = $name; + $instance->settings = $settings['mysql']; + $instance->initDbConnection(); + $instance->prepareTable(); + + if (!empty($value) && !$value instanceof static) { + if ($value instanceof DbArray) { + $value = $value->getArrayCopy(); + } + foreach ((array) $value as $key => $item) { + $instance[$key] = $item; + } + } + + + return $instance; + } + + /** + * Check if offset exists + * + * @link https://php.net/manual/en/arrayiterator.offsetexists.php + * + * @param string $index

+ * The offset being checked. + *

+ * + * @return bool true if the offset exists, otherwise false + * @throws \Throwable + */ + public function offsetExists($index) + { + $row = $this->syncRequest( + "SELECT count(`key`) as `count` FROM {$this->table} WHERE `key` = :index LIMIT 1", + ['index' => $index] + ); + + $row = reset($row); + return !empty($row['count']); + } + + /** + * Get value for an offset + * + * @link https://php.net/manual/en/arrayiterator.offsetget.php + * + * @param string $index

+ * The offset to get the value from. + *

+ * + * @return mixed The value at offset index. + * @throws \Throwable + */ + public function offsetGet($index) + { + $row = $this->syncRequest( + "SELECT `value` FROM {$this->table} WHERE `key` = :index LIMIT 1", + ['index' => $index] + ); + $row = reset($row); + if ($row) { + return unserialize($row['value']); + } + return null; + + } + + /** + * Set value for an offset + * + * @link https://php.net/manual/en/arrayiterator.offsetset.php + * + * @param string $index

+ * The index to set for. + *

+ * @param $value + * + * @return void + * @throws \Throwable + */ + public function offsetSet($index, $value) + { + $this->syncRequest(" + INSERT INTO `{$this->table}` + SET `key` = :index, `value` = :value + ON DUPLICATE KEY UPDATE `value` = :value + ", + [ + 'index' => $index, + 'value' => serialize($value), + ] + ); + } + + /** + * Unset value for an offset + * + * @link https://php.net/manual/en/arrayiterator.offsetunset.php + * + * @param string $index

+ * The offset to unset. + *

+ * + * @return void + * @throws \Throwable + */ + public function offsetUnset($index) + { + $this->syncRequest(" + DELETE FROM `{$this->table}` + WHERE `key` = :index + ", + ['index' => $index] + ); + } + + /** + * Append an element + * @link https://php.net/manual/en/arrayiterator.append.php + * @param mixed $value

+ * The value to append. + *

+ * @return void + */ + public function append($value) + { + throw new \BadMethodCallException('Append operation does not supported'); + } + + /** + * Get array copy + * + * @link https://php.net/manual/en/arrayiterator.getarraycopy.php + * @return array A copy of the array, or array of public properties + * if ArrayIterator refers to an object. + * @throws \Throwable + */ + public function getArrayCopy(): array + { + $rows = $this->syncRequest("SELECT `key`, `value` FROM {$this->table}"); + $result = []; + foreach ($rows as $row) { + $result[$row['key']] = unserialize($row['value']); + } + return $result; + } + + /** + * Count elements + * + * @link https://php.net/manual/en/arrayiterator.count.php + * @return int The number of elements or public properties in the associated + * array or object, respectively. + * @throws \Throwable + */ + public function count(): int + { + return $this->syncRequest("SELECT count(`key`) as `count` FROM {$this->table}")['count'] ?? 0; + } + + /** + * Sort array by values + * @link https://php.net/manual/en/arrayiterator.asort.php + * @return void + */ + public function asort() + { + throw new \BadMethodCallException('Sort operation does not supported'); + } + + /** + * Sort array by keys + * @link https://php.net/manual/en/arrayiterator.ksort.php + * @return void + */ + public function ksort() + { + throw new \BadMethodCallException('Sort operation does not supported'); + } + + /** + * User defined sort + * @link https://php.net/manual/en/arrayiterator.uasort.php + * @param string $cmp_function

+ * The compare function used for the sort. + *

+ * @return void + */ + public function uasort($cmp_function) + { + throw new \BadMethodCallException('Sort operation does not supported'); + } + + /** + * User defined sort + * @link https://php.net/manual/en/arrayiterator.uksort.php + * @param string $cmp_function

+ * The compare function used for the sort. + *

+ * @return void + */ + public function uksort($cmp_function) + { + throw new \BadMethodCallException('Sort operation does not supported'); + } + + /** + * Sort an array naturally + * @link https://php.net/manual/en/arrayiterator.natsort.php + * @return void + */ + public function natsort() + { + throw new \BadMethodCallException('Sort operation does not supported'); + } + + /** + * Sort an array naturally, case insensitive + * @link https://php.net/manual/en/arrayiterator.natcasesort.php + * @return void + */ + public function natcasesort() + { + throw new \BadMethodCallException('Sort operation does not supported'); + } + + /** + * Rewind array back to the start + * + * @link https://php.net/manual/en/arrayiterator.rewind.php + * @return void + * @throws \Throwable + */ + public function rewind() + { + $this->key = null; + $this->key(); + } + + /** + * Return current array entry + * + * @link https://php.net/manual/en/arrayiterator.current.php + * @return mixed The current array entry. + * @throws \Throwable + */ + public function current() + { + return $this->offsetGet($this->key()); + } + + /** + * Return current array key + * + * @link https://php.net/manual/en/arrayiterator.key.php + * @return string|float|int|bool|null The current array key. + * @throws \Throwable + */ + public function key(): ?string + { + if ($this->key === null) { + $row = $this->syncRequest( + "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1" + ); + if ($row) { + $row = reset($row); + $this->key = $row['key'] ?? null; + } + + } + return $this->key; + } + + /** + * Move to next entry + * + * @link https://php.net/manual/en/arrayiterator.next.php + * @return void + * @throws \Throwable + */ + public function next() { + $row = $this->syncRequest( + "SELECT `key` FROM {$this->table} WHERE `key` > :key LIMIT 1", + ['key' => $this->key()] + ); + $row = reset($row); + $this->key = $row['key'] ?? null; + } + + /** + * Check whether array contains more entries + * + * @link https://php.net/manual/en/arrayiterator.valid.php + * @return bool + * @throws \Throwable + */ + public function valid() { + if ($this->key() === null) { + return false; + } + + $row = $this->syncRequest( + "SELECT `key` FROM {$this->table} WHERE `key` > :key LIMIT 1", + ['key' => $this->key()] + ); + + return $row !== null; + } + + /** + * Seek to position + * @link https://php.net/manual/en/arrayiterator.seek.php + * @param int $position

+ * The position to seek to. + *

+ * @return void + */ + public function seek($position) + { + $row = $this->syncRequest( + "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1, :position", + ['offset' => $position] + ); + $row = reset($row); + if (isset($row['key'])) { + $this->key = $row['key']; + } + } + + private function initDbConnection() + { + $this->db = Mysql::getConnection( + $this->settings['host'], + $this->settings['port'], + $this->settings['user'], + $this->settings['password'], + $this->settings['database'], + ); + } + + /** + * Create table for property + * + * @return array|null + * @throws \Throwable + */ + private function prepareTable() + { + return $this->syncRequest(" + CREATE TABLE IF NOT EXISTS `{$this->table}` + ( + `key` VARCHAR(255) NOT NULL, + `value` LONGTEXT NULL, + `ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`key`) + ) + "); + } + + /** + * Perform blocking request to db + * + * @param string $query + * @param array $params + * + * @return array|null + * @throws \Throwable + */ + private function syncRequest(string $query, array $params = []): array + { + return Tools::wait( + call( + function() use($query, $params) { + $request = yield $this->db->execute($query, $params); + $result = []; + if ($request instanceof ResultSet) { + while (yield $request->advance()) { + $row = $request->getCurrent(); + if (isset($row['key'])) { + $result[$row['key']] = $row; + } else { + $result[] = $row; + } + + } + } + return $result; + } + ) + ); + } + +} \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/SharedMemoryArray.php b/src/danog/MadelineProto/Db/SharedMemoryArray.php new file mode 100644 index 00000000..2252556c --- /dev/null +++ b/src/danog/MadelineProto/Db/SharedMemoryArray.php @@ -0,0 +1,25 @@ +getArrayCopy(); + } + $value = array_replace_recursive(static::$instance->getArrayCopy(), (array) $value); + foreach ($value as $key => $item) { + static::$instance[$key] = $item; + } + } + + return static::$instance; + } +} \ No newline at end of file diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 3a17eda4..a6c43204 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -23,6 +23,12 @@ use Amp\Dns\Resolver; use Amp\File\StatCache; use Amp\Http\Client\HttpClient; use danog\MadelineProto\Async\AsyncConstruct; +use danog\MadelineProto\Db\DbArray; +use danog\MadelineProto\Db\DbType; +use danog\MadelineProto\Db\Engines\DbInterface; +use danog\MadelineProto\Db\DbPropertiesFabric; +use danog\MadelineProto\Db\Mysql; +use danog\MadelineProto\Db\Types\ArrayType; use danog\MadelineProto\Loop\Generic\PeriodicLoop; use danog\MadelineProto\Loop\Update\FeedLoop; use danog\MadelineProto\Loop\Update\SeqLoop; @@ -85,7 +91,7 @@ class MTProto extends AsyncConstruct implements TLCallback * * @var int */ - const V = 138; + const V = 139; /** * String release version. * @@ -278,15 +284,15 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Internal peer database. * - * @var array + * @var DbArray */ - public $chats = []; + public $chats; /** * Cached parameters for fetching channel participants. * - * @var array + * @var DbArray */ - public $channel_participants = []; + public $channel_participants; /** * When we last stored data in remote peer database (now doesn't exist anymore). * @@ -302,9 +308,9 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Full chat info database. * - * @var array + * @var DbArray */ - public $full_chats = []; + public $full_chats; /** * Latest chat message ID map for update handling. * @@ -407,6 +413,18 @@ class MTProto extends AsyncConstruct implements TLCallback * @var \danog\MadelineProto\TL\TL */ private $TL; + + /** + * List of properties stored in database (memory or external) + * @see DbPropertiesFabric + * @var array + */ + private array $dbProperies = [ + 'chats' => 'array', + 'full_chats' => 'array', + 'channel_participants' => 'array' + ]; + /** * Constructor function. * @@ -540,6 +558,18 @@ class MTProto extends AsyncConstruct implements TLCallback 'reportDest' ]; } + + public function initDb(bool $reset = false): void + { + foreach ($this->dbProperies as $property => $type) { + if ($reset) { + unset($this->{$property}); + } else { + $this->{$property} = DbPropertiesFabric::get($this->settings['db'], $type, $property, $this->{$property}); + } + } + } + /** * Cleanup memory and session file. * @@ -751,6 +781,9 @@ class MTProto extends AsyncConstruct implements TLCallback } $this->TL->init($this->settings['tl_schema']['src'], $callbacks); } + + $this->initDb(); + } /** * Upgrade MadelineProto instance. @@ -777,9 +810,9 @@ class MTProto extends AsyncConstruct implements TLCallback if (isset($settings['authorization']['rsa_key'])) { unset($settings['authorization']['rsa_key']); } - if (!isset($this->full_chats)) { - $this->full_chats = []; - } + + $this->initDb(); + if (!isset($this->secret_chats)) { $this->secret_chats = []; } @@ -799,6 +832,8 @@ class MTProto extends AsyncConstruct implements TLCallback $chat['mtproto'] = 1; } } + unset($chat); + foreach ($settings['connection_settings'] as $key => &$connection) { if (\in_array($key, ['default_dc', 'media_socket_count', 'robin_period'])) { continue; @@ -821,6 +856,8 @@ class MTProto extends AsyncConstruct implements TLCallback $connection['obfuscated'] = true; } } + unset($connection); + $this->resetMTProtoSession(true, true); $this->config = ['expires' => -1]; $this->dh_config = ['version' => 0]; @@ -1244,6 +1281,23 @@ class MTProto extends AsyncConstruct implements TLCallback 'run_callback' => true, ], 'secret_chats' => ['accept_chats' => true], 'serialization' => ['serialization_interval' => 30, 'cleanup_before_serialization' => false], + /** + * Where internal database will be stored? + * memory - session file + * sharedMemory - multiples instances share db if run in single process + * mysql - mysql database, shared by all instances in all processes. + */ + 'db' => [ + 'type' => 'memory', + /** @see Mysql */ + 'mysql' => [ + 'host' => '127.0.0.1', + 'port' => 3306, + 'user' => 'root', + 'password' => '', + 'database' => 'MadelineProto' + ] + ], 'upload' => ['allow_automatic_upload' => true, 'part_size' => 512 * 1024, 'parallel_chunks' => 20], 'download' => ['report_broken_media' => true, 'part_size' => 1024 * 1024, 'parallel_chunks' => 20], 'pwr' => [ 'pwr' => false, // Need info ? @@ -1469,13 +1523,13 @@ class MTProto extends AsyncConstruct implements TLCallback $this->authorization = null; $this->updates = []; $this->secret_chats = []; - $this->chats = []; - $this->users = []; + + $this->initDb(true); + $this->tos = ['expires' => 0, 'accepted' => true]; $this->referenceDatabase = new ReferenceDatabase($this); $this->minDatabase = new MinDatabase($this); $this->dialog_params = ['_' => 'MadelineProto.dialogParams', 'limit' => 0, 'offset_date' => 0, 'offset_id' => 0, 'offset_peer' => ['_' => 'inputPeerEmpty'], 'count' => 0]; - $this->full_chats = []; } /** * Reset the update state and fetch all updates from the beginning. From 0d191f4157fddc8af6db87309de40b10f88406b1 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Mon, 27 Apr 2020 03:21:18 +0300 Subject: [PATCH 04/25] Bugfixes and optimizations --- src/danog/MadelineProto/Db/DbArray.php | 7 +- src/danog/MadelineProto/Db/MemoryArray.php | 6 +- src/danog/MadelineProto/Db/Mysql.php | 8 +- src/danog/MadelineProto/Db/MysqlArray.php | 155 ++++-------------- .../MadelineProto/Db/SharedMemoryArray.php | 7 +- src/danog/MadelineProto/MTProto.php | 18 +- .../MTProtoTools/PeerHandler.php | 18 +- .../MTProtoTools/ReferenceDatabase.php | 4 +- .../MTProtoTools/UpdateHandler.php | 4 +- .../MadelineProto/Wrappers/DialogHandler.php | 7 +- 10 files changed, 97 insertions(+), 137 deletions(-) diff --git a/src/danog/MadelineProto/Db/DbArray.php b/src/danog/MadelineProto/Db/DbArray.php index 263d9a93..631bb302 100644 --- a/src/danog/MadelineProto/Db/DbArray.php +++ b/src/danog/MadelineProto/Db/DbArray.php @@ -2,10 +2,7 @@ namespace danog\MadelineProto\Db; -abstract class DbArray extends \ArrayIterator implements DbType +interface DbArray extends DbType, \ArrayAccess, \Countable, \Iterator, \SeekableIterator { - protected function __construct($array = [], $flags = 0) - { - parent::__construct((array) $array, $flags | self::STD_PROP_LIST); - } + public function getArrayCopy(); } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MemoryArray.php b/src/danog/MadelineProto/Db/MemoryArray.php index 6cab009b..658edc56 100644 --- a/src/danog/MadelineProto/Db/MemoryArray.php +++ b/src/danog/MadelineProto/Db/MemoryArray.php @@ -2,8 +2,12 @@ namespace danog\MadelineProto\Db; -class MemoryArray extends DbArray +class MemoryArray extends \ArrayIterator implements DbArray { + protected function __construct($array = [], $flags = 0) + { + parent::__construct((array) $array, $flags | self::STD_PROP_LIST); + } static function getInstance(array $settings, string $name, $value = []): DbArray { diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php index cada90af..4374d71e 100644 --- a/src/danog/MadelineProto/Db/Mysql.php +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -2,7 +2,6 @@ namespace danog\MadelineProto\Db; -use Amp\Loop; use Amp\Mysql\ConnectionConfig; use Amp\Mysql\Pool; use function Amp\Mysql\Pool; @@ -42,4 +41,11 @@ class Mysql return static::$connections[$dbKey]; } + public function __destruct() + { + foreach (static::$connections as $connection) { + $connection->close(); + } + } + } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 83ec3023..0bb89057 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -7,18 +7,18 @@ use Amp\Sql\ResultSet; use danog\MadelineProto\Tools; use function Amp\call; -class MysqlArray extends DbArray +class MysqlArray implements DbArray { private string $table; private array $settings; private Pool $db; private ?string $key = null; + private $current; public function __serialize(): array { return [ 'table' => $this->table, - 'key' => $this->key, 'settings' => $this->settings ]; } @@ -71,8 +71,7 @@ class MysqlArray extends DbArray ['index' => $index] ); - $row = reset($row); - return !empty($row['count']); + return !empty($row[0]['count']); } /** @@ -93,11 +92,7 @@ class MysqlArray extends DbArray "SELECT `value` FROM {$this->table} WHERE `key` = :index LIMIT 1", ['index' => $index] ); - $row = reset($row); - if ($row) { - return unserialize($row['value']); - } - return null; + return $this->getValue($row); } @@ -150,19 +145,6 @@ class MysqlArray extends DbArray ); } - /** - * Append an element - * @link https://php.net/manual/en/arrayiterator.append.php - * @param mixed $value

- * The value to append. - *

- * @return void - */ - public function append($value) - { - throw new \BadMethodCallException('Append operation does not supported'); - } - /** * Get array copy * @@ -178,6 +160,7 @@ class MysqlArray extends DbArray foreach ($rows as $row) { $result[$row['key']] = unserialize($row['value']); } + return $result; } @@ -191,73 +174,8 @@ class MysqlArray extends DbArray */ public function count(): int { - return $this->syncRequest("SELECT count(`key`) as `count` FROM {$this->table}")['count'] ?? 0; - } - - /** - * Sort array by values - * @link https://php.net/manual/en/arrayiterator.asort.php - * @return void - */ - public function asort() - { - throw new \BadMethodCallException('Sort operation does not supported'); - } - - /** - * Sort array by keys - * @link https://php.net/manual/en/arrayiterator.ksort.php - * @return void - */ - public function ksort() - { - throw new \BadMethodCallException('Sort operation does not supported'); - } - - /** - * User defined sort - * @link https://php.net/manual/en/arrayiterator.uasort.php - * @param string $cmp_function

- * The compare function used for the sort. - *

- * @return void - */ - public function uasort($cmp_function) - { - throw new \BadMethodCallException('Sort operation does not supported'); - } - - /** - * User defined sort - * @link https://php.net/manual/en/arrayiterator.uksort.php - * @param string $cmp_function

- * The compare function used for the sort. - *

- * @return void - */ - public function uksort($cmp_function) - { - throw new \BadMethodCallException('Sort operation does not supported'); - } - - /** - * Sort an array naturally - * @link https://php.net/manual/en/arrayiterator.natsort.php - * @return void - */ - public function natsort() - { - throw new \BadMethodCallException('Sort operation does not supported'); - } - - /** - * Sort an array naturally, case insensitive - * @link https://php.net/manual/en/arrayiterator.natcasesort.php - * @return void - */ - public function natcasesort() - { - throw new \BadMethodCallException('Sort operation does not supported'); + $row = $this->syncRequest("SELECT count(`key`) as `count` FROM {$this->table}"); + return $row[0]['count'] ?? 0; } /** @@ -271,6 +189,7 @@ class MysqlArray extends DbArray { $this->key = null; $this->key(); + $this->current = null; } /** @@ -282,7 +201,16 @@ class MysqlArray extends DbArray */ public function current() { - return $this->offsetGet($this->key()); + return $this->current ?: $this->offsetGet($this->key()); + } + + private function getValue(array $row) + { + if ($row) { + $row = reset($row); + return unserialize($row['value']); + } + return null; } /** @@ -298,11 +226,7 @@ class MysqlArray extends DbArray $row = $this->syncRequest( "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1" ); - if ($row) { - $row = reset($row); - $this->key = $row['key'] ?? null; - } - + $this->key = $row[0]['key'] ?? null; } return $this->key; } @@ -314,13 +238,15 @@ class MysqlArray extends DbArray * @return void * @throws \Throwable */ - public function next() { + public function next() + { $row = $this->syncRequest( - "SELECT `key` FROM {$this->table} WHERE `key` > :key LIMIT 1", + "SELECT `key`, `value` FROM {$this->table} WHERE `key` > :key ORDER BY `key` LIMIT 1", ['key' => $this->key()] ); - $row = reset($row); - $this->key = $row['key'] ?? null; + + $this->key = $row[0]['key'] ?? null; + $this->current = $this->getValue($row); } /** @@ -330,17 +256,9 @@ class MysqlArray extends DbArray * @return bool * @throws \Throwable */ - public function valid() { - if ($this->key() === null) { - return false; - } - - $row = $this->syncRequest( - "SELECT `key` FROM {$this->table} WHERE `key` > :key LIMIT 1", - ['key' => $this->key()] - ); - - return $row !== null; + public function valid():bool + { + return $this->key !== null; } /** @@ -357,14 +275,12 @@ class MysqlArray extends DbArray "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1, :position", ['offset' => $position] ); - $row = reset($row); - if (isset($row['key'])) { - $this->key = $row['key']; - } + $this->key = $row[0]['key'] ?? $this->key; } private function initDbConnection() { + //TODO Use MtProto::$settings $this->db = Mysql::getConnection( $this->settings['host'], $this->settings['port'], @@ -386,7 +302,7 @@ class MysqlArray extends DbArray CREATE TABLE IF NOT EXISTS `{$this->table}` ( `key` VARCHAR(255) NOT NULL, - `value` LONGTEXT NULL, + `value` MEDIUMBLOB NULL, `ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`key`) ) @@ -411,13 +327,7 @@ class MysqlArray extends DbArray $result = []; if ($request instanceof ResultSet) { while (yield $request->advance()) { - $row = $request->getCurrent(); - if (isset($row['key'])) { - $result[$row['key']] = $row; - } else { - $result[] = $row; - } - + $result[] = $request->getCurrent(); } } return $result; @@ -425,5 +335,4 @@ class MysqlArray extends DbArray ) ); } - } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/SharedMemoryArray.php b/src/danog/MadelineProto/Db/SharedMemoryArray.php index 2252556c..2d82c1dc 100644 --- a/src/danog/MadelineProto/Db/SharedMemoryArray.php +++ b/src/danog/MadelineProto/Db/SharedMemoryArray.php @@ -2,10 +2,15 @@ namespace danog\MadelineProto\Db; -class SharedMemoryArray extends DbArray +class SharedMemoryArray extends \ArrayIterator implements DbArray { private static SharedMemoryArray $instance; + protected function __construct($array = [], $flags = 0) + { + parent::__construct((array) $array, $flags | self::STD_PROP_LIST); + } + public static function getInstance(array $settings, string $name, $value = []): DbArray { if (empty(static::$instance)) { diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index a6c43204..c738173f 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -24,7 +24,6 @@ use Amp\File\StatCache; use Amp\Http\Client\HttpClient; use danog\MadelineProto\Async\AsyncConstruct; use danog\MadelineProto\Db\DbArray; -use danog\MadelineProto\Db\DbType; use danog\MadelineProto\Db\Engines\DbInterface; use danog\MadelineProto\Db\DbPropertiesFabric; use danog\MadelineProto\Db\Mysql; @@ -422,7 +421,12 @@ class MTProto extends AsyncConstruct implements TLCallback private array $dbProperies = [ 'chats' => 'array', 'full_chats' => 'array', - 'channel_participants' => 'array' + 'channel_participants' => 'array', + 'caching_simple' => 'array', + 'caching_simple_username' => 'array', + 'caching_possible_username' => 'array', + 'caching_full_info' => 'array', + 'caching_username_id' => 'array', ]; /** @@ -568,6 +572,16 @@ class MTProto extends AsyncConstruct implements TLCallback $this->{$property} = DbPropertiesFabric::get($this->settings['db'], $type, $property, $this->{$property}); } } + + if (!$reset && count($this->caching_username_id) === 0) { + $this->logger('Filling database cache. This can take few minutes.', Logger::WARNING); + foreach ($this->chats as $id => $chat) { + if (isset($chat['username'])) { + $this->caching_username_id[$chat['username']] = $id; + } + } + $this->logger('Cache filled.', Logger::WARNING); + } } /** diff --git a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php index debf2f8f..cdb395e4 100644 --- a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php @@ -34,6 +34,8 @@ trait PeerHandler public $caching_simple_username = []; public $caching_possible_username = []; public $caching_full_info = []; + public $caching_username_id = []; + /** * Convert MTProto channel ID to bot API channel ID. * @@ -562,7 +564,19 @@ trait PeerHandler } return yield from $this->getInfo($this->supportUser); } + if ($bot_api_id = $this->caching_username_id[$id] ?? null) { + $chat = $this->chats[$bot_api_id]; + if (empty($chat['username']) || $chat['username'] !== $id) { + unset($this->caching_username_id[$id]); + } else { + return $this->genAll($this->chats[$bot_api_id], $folder_id); + } + } + foreach ($this->chats as $bot_api_id => $chat) { + if (isset($chat['username'])) { + $this->caching_username_id[$id] = $bot_api_id; + } if (isset($chat['username']) && \strtolower($chat['username']) === $id) { if ($chat['min'] ?? false && !isset($this->caching_full_info[$bot_api_id])) { $this->caching_full_info[$bot_api_id] = true; @@ -969,7 +983,9 @@ trait PeerHandler } \sort($ids, SORT_NUMERIC); $gres['hash'] = \danog\MadelineProto\Tools::genVectorHash($ids); - $this->channel_participants[$channel['channel_id']][$filter][$q][$offset][$limit] = $gres; + $participant = $this->channel_participants[$channel['channel_id']]; + $participant[$filter][$q][$offset][$limit] = $gres; + $this->channel_participants[$channel['channel_id']] = $participant; } private function getParticipantsHash($channel, $filter, $q, $offset, $limit) { diff --git a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php index 236fb4f9..72dd357b 100644 --- a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php +++ b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php @@ -423,7 +423,9 @@ class ReferenceDatabase implements TLCallback // Peer + photo ID case self::PEER_PHOTO_ORIGIN: if (isset($this->API->full_chats[$origin['peer']]['last_update'])) { - $this->API->full_chats[$origin['peer']]['last_update'] = 0; + $chat = $this->API->full_chats[$origin['peer']]; + $chat['last_update'] = 0; + $this->API->full_chats[$origin['peer']] = $chat; } $this->API->getFullInfo($origin['peer']); break; diff --git a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php index 67424f42..facb7475 100644 --- a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php @@ -332,7 +332,9 @@ trait UpdateHandler } if (\in_array($update['_'], ['updateUserName', 'updateUserPhone', 'updateUserBlocked', 'updateUserPhoto', 'updateContactRegistered', 'updateContactLink'])) { $id = $this->getId($update); - $this->full_chats[$id]['last_update'] = 0; + $chat = $this->full_chats[$id]; + $chat['last_update'] = 0; + $this->full_chats[$id] = $chat; yield from $this->getFullInfo($id); } if ($update['_'] === 'updateDcOptions') { diff --git a/src/danog/MadelineProto/Wrappers/DialogHandler.php b/src/danog/MadelineProto/Wrappers/DialogHandler.php index c9d80767..d7de6417 100644 --- a/src/danog/MadelineProto/Wrappers/DialogHandler.php +++ b/src/danog/MadelineProto/Wrappers/DialogHandler.php @@ -33,7 +33,12 @@ trait DialogHandler if ($this->authorization['user']['bot']) { $res = []; foreach ($this->chats as $chat) { - $res[] = $this->genAll($chat)['Peer']; + try { + $res[] = $this->genAll($chat)['Peer']; + } catch (\Throwable $e) { + continue; + } + } return $res; } From 26abf9f04e895e89a804049f934bf7bed5757011 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Tue, 28 Apr 2020 03:41:06 +0300 Subject: [PATCH 05/25] Bugfixes and optimizations --- src/danog/MadelineProto/Db/DbArray.php | 6 + .../MadelineProto/Db/DbPropertiesFabric.php | 16 +- src/danog/MadelineProto/Db/DbType.php | 2 +- src/danog/MadelineProto/Db/MemoryArray.php | 30 ++- src/danog/MadelineProto/Db/Mysql.php | 7 - src/danog/MadelineProto/Db/MysqlArray.php | 174 ++++++++++++++---- .../MadelineProto/Db/SharedMemoryArray.php | 30 --- src/danog/MadelineProto/MTProto.php | 39 ++-- .../MTProtoTools/PeerHandler.php | 26 +-- 9 files changed, 216 insertions(+), 114 deletions(-) delete mode 100644 src/danog/MadelineProto/Db/SharedMemoryArray.php diff --git a/src/danog/MadelineProto/Db/DbArray.php b/src/danog/MadelineProto/Db/DbArray.php index 631bb302..27b7e538 100644 --- a/src/danog/MadelineProto/Db/DbArray.php +++ b/src/danog/MadelineProto/Db/DbArray.php @@ -2,7 +2,13 @@ namespace danog\MadelineProto\Db; +use Amp\Producer; +use Amp\Promise; + interface DbArray extends DbType, \ArrayAccess, \Countable, \Iterator, \SeekableIterator { public function getArrayCopy(); + public function offsetGetAsync(string $offset): Promise; + public function offsetSetAsync(string $offset, $value): Promise; + public function getIterator(): Producer; } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/DbPropertiesFabric.php b/src/danog/MadelineProto/Db/DbPropertiesFabric.php index 8ac5754b..6739ce3e 100644 --- a/src/danog/MadelineProto/Db/DbPropertiesFabric.php +++ b/src/danog/MadelineProto/Db/DbPropertiesFabric.php @@ -2,10 +2,13 @@ namespace danog\MadelineProto\Db; +use danog\MadelineProto\API; +use danog\MadelineProto\MTProto; + class DbPropertiesFabric { /** - * @param array $dbSettings + * @param MTProto $madelineProto * @param string $propertyType * @param string $name * @param $value @@ -16,16 +19,14 @@ class DbPropertiesFabric * @uses \danog\MadelineProto\Db\SharedMemoryArray * @uses \danog\MadelineProto\Db\MysqlArray */ - public static function get(array $dbSettings, string $propertyType, string $name, $value = null): DbType + public static function get(MTProto $madelineProto, string $propertyType, string $name, $value = null): DbType { $class = __NAMESPACE__; + $dbSettings = $madelineProto->settings['db']; switch (strtolower($dbSettings['type'])) { case 'memory': $class .= '\Memory'; break; - case 'sharedmemory': - $class .= '\SharedMemory'; - break; case 'mysql': $class .= '\Mysql'; break; @@ -34,6 +35,7 @@ class DbPropertiesFabric } + /** @var DbType $class */ switch (strtolower($propertyType)){ case 'array': $class .= 'Array'; @@ -42,8 +44,8 @@ class DbPropertiesFabric throw new \InvalidArgumentException("Unknown $propertyType: {$propertyType}"); } - /** @var DbType $class */ - return $class::getInstance($dbSettings, $name, $value); + $prefix = (string) ($madelineProto->getSelf()['id'] ?? 'tmp'); + return $class::getInstance($name, $value, $prefix, $dbSettings[$dbSettings['type']]??[]); } } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/DbType.php b/src/danog/MadelineProto/Db/DbType.php index e9bf5dcc..bab9d584 100644 --- a/src/danog/MadelineProto/Db/DbType.php +++ b/src/danog/MadelineProto/Db/DbType.php @@ -4,5 +4,5 @@ namespace danog\MadelineProto\Db; interface DbType { - static function getInstance(array $settings, string $name, $value): self; + static function getInstance(string $name, $value, string $tablePrefix, array $settings): self; } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MemoryArray.php b/src/danog/MadelineProto/Db/MemoryArray.php index 658edc56..39d94ac1 100644 --- a/src/danog/MadelineProto/Db/MemoryArray.php +++ b/src/danog/MadelineProto/Db/MemoryArray.php @@ -2,6 +2,10 @@ namespace danog\MadelineProto\Db; +use Amp\Producer; +use Amp\Promise; +use function Amp\call; + class MemoryArray extends \ArrayIterator implements DbArray { protected function __construct($array = [], $flags = 0) @@ -9,11 +13,35 @@ class MemoryArray extends \ArrayIterator implements DbArray parent::__construct((array) $array, $flags | self::STD_PROP_LIST); } - static function getInstance(array $settings, string $name, $value = []): DbArray + public static function getInstance(string $name, $value, string $tablePrefix, array $settings): DbArray { if ($value instanceof DbArray) { $value = $value->getArrayCopy(); } return new static($value); } + + public static function getDbConnection(array $settings) + { + return null; + } + + public function offsetGetAsync(string $offset): Promise + { + return call(fn() => $this->offsetGet($offset)); + } + + public function offsetSetAsync(string $offset, $value): Promise + { + return call(fn() => $this->offsetSet($offset, $value)); + } + + public function getIterator(): Producer + { + return new Producer(function (callable $emit) { + foreach ($this as $value) { + yield $emit($value); + } + }); + } } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php index 4374d71e..569cebe0 100644 --- a/src/danog/MadelineProto/Db/Mysql.php +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -41,11 +41,4 @@ class Mysql return static::$connections[$dbKey]; } - public function __destruct() - { - foreach (static::$connections as $connection) { - $connection->close(); - } - } - } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 0bb89057..d9763efc 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -2,10 +2,15 @@ namespace danog\MadelineProto\Db; +use Amp\Loop; use Amp\Mysql\Pool; +use Amp\Producer; +use Amp\Promise; use Amp\Sql\ResultSet; +use danog\MadelineProto\Logger; use danog\MadelineProto\Tools; use function Amp\call; +use function Amp\Promise\wait; class MysqlArray implements DbArray { @@ -28,25 +33,51 @@ class MysqlArray implements DbArray foreach ($data as $property => $value) { $this->{$property} = $value; } - $this->initDbConnection(); + try { + $this->db = static::getDbConnection($this->settings); + } catch (\Throwable $e) { + Logger::log($e->getMessage(), Logger::ERROR); + } + } - public static function getInstance(array $settings, string $name, $value = []): DbType + public static function getInstance(string $name, $value, string $tablePrefix, array $settings): DbType { $instance = new static(); - $instance->table = $name; - $instance->settings = $settings['mysql']; - $instance->initDbConnection(); - $instance->prepareTable(); - if (!empty($value) && !$value instanceof static) { - if ($value instanceof DbArray) { - $value = $value->getArrayCopy(); - } - foreach ((array) $value as $key => $item) { - $instance[$key] = $item; + $instance->table = "{$tablePrefix}_{$name}"; + $instance->settings = $settings; + $instance->db = static::getDbConnection($settings); + + if ($value instanceof static) { + if ($instance->table !== $value->table) { + $instance->renameTable($value->table, $instance->table); } } + $instance->prepareTable(); + + Loop::defer(function() use($value, $instance){ + if (!empty($value) && !$value instanceof static) { + Logger::log('Converting database.', Logger::ERROR); + if ($value instanceof DbArray) { + $value = $value->getArrayCopy(); + } + $value = (array) $value; + $counter = 0; + $total = count($value); + foreach ((array) $value as $key => $item) { + $counter++; + if ($counter % 100 === 0) { + yield $instance->offsetSetAsync($key, $item); + Logger::log("Converting database. $counter/$total", Logger::WARNING); + } else { + $instance->offsetSetAsync($key, $item); + } + + } + Logger::log('Converting database done.', Logger::ERROR); + } + }); return $instance; @@ -88,12 +119,19 @@ class MysqlArray implements DbArray */ public function offsetGet($index) { - $row = $this->syncRequest( - "SELECT `value` FROM {$this->table} WHERE `key` = :index LIMIT 1", - ['index' => $index] - ); - return $this->getValue($row); + return wait($this->offsetGetAsync($index)); + } + + public function offsetGetAsync(string $offset): Promise + { + return call(function() use($offset) { + $row = yield $this->request( + "SELECT `value` FROM {$this->table} WHERE `key` = :index LIMIT 1", + ['index' => $offset] + ); + return $this->getValue($row); + }); } /** @@ -123,6 +161,20 @@ class MysqlArray implements DbArray ); } + public function offsetSetAsync($index, $value): Promise + { + return $this->request(" + INSERT INTO `{$this->table}` + SET `key` = :index, `value` = :value + ON DUPLICATE KEY UPDATE `value` = :value + ", + [ + 'index' => $index, + 'value' => serialize($value), + ] + ); + } + /** * Unset value for an offset * @@ -164,6 +216,19 @@ class MysqlArray implements DbArray return $result; } + public function getIterator(): Producer + { + return new Producer(function (callable $emit) { + $request = yield $this->db->execute("SELECT `key`, `value` FROM {$this->table}"); + + while (yield $request->advance()) { + $row = $request->getCurrent(); + + yield $emit($this->getValue($row)); + } + }); + } + /** * Count elements * @@ -207,7 +272,9 @@ class MysqlArray implements DbArray private function getValue(array $row) { if ($row) { - $row = reset($row); + if (!empty($row[0]['value'])) { + $row = reset($row); + } return unserialize($row['value']); } return null; @@ -272,21 +339,20 @@ class MysqlArray implements DbArray public function seek($position) { $row = $this->syncRequest( - "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1, :position", + "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1 OFFSET :position", ['offset' => $position] ); $this->key = $row[0]['key'] ?? $this->key; } - private function initDbConnection() + public static function getDbConnection(array $settings): Pool { - //TODO Use MtProto::$settings - $this->db = Mysql::getConnection( - $this->settings['host'], - $this->settings['port'], - $this->settings['user'], - $this->settings['password'], - $this->settings['database'], + return Mysql::getConnection( + $settings['host'], + $settings['port'], + $settings['user'], + $settings['password'], + $settings['database'], ); } @@ -309,6 +375,18 @@ class MysqlArray implements DbArray "); } + private function renameTable(string $from, string $to) + { + try { + $this->syncRequest(" + ALTER TABLE {$from} RENAME TO {$to}; + "); + } catch (\Throwable $e) { + Logger::log("Cant rename table {$from} to {$to}", Logger::WARNING); + } + + } + /** * Perform blocking request to db * @@ -320,19 +398,35 @@ class MysqlArray implements DbArray */ private function syncRequest(string $query, array $params = []): array { - return Tools::wait( - call( - function() use($query, $params) { - $request = yield $this->db->execute($query, $params); - $result = []; - if ($request instanceof ResultSet) { - while (yield $request->advance()) { - $result[] = $request->getCurrent(); - } - } - return $result; + return Tools::wait($this->request($query, $params)); + } + + /** + * Perform blocking request to db + * + * @param string $query + * @param array $params + * + * @return Promise + * @throws \Throwable + */ + private function request(string $query, array $params = []): Promise + { + return call(function() use($query, $params) { + if (empty($this->db)) { + return []; + } + + $request = yield $this->db->execute($query, $params); + $result = []; + if ($request instanceof ResultSet) { + while (yield $request->advance()) { + $result[] = $request->getCurrent(); } - ) - ); + } + return $result; + }); + + } } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/SharedMemoryArray.php b/src/danog/MadelineProto/Db/SharedMemoryArray.php deleted file mode 100644 index 2d82c1dc..00000000 --- a/src/danog/MadelineProto/Db/SharedMemoryArray.php +++ /dev/null @@ -1,30 +0,0 @@ -getArrayCopy(); - } - $value = array_replace_recursive(static::$instance->getArrayCopy(), (array) $value); - foreach ($value as $key => $item) { - static::$instance[$key] = $item; - } - } - - return static::$instance; - } -} \ No newline at end of file diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index c738173f..45aa8362 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -24,10 +24,8 @@ use Amp\File\StatCache; use Amp\Http\Client\HttpClient; use danog\MadelineProto\Async\AsyncConstruct; use danog\MadelineProto\Db\DbArray; -use danog\MadelineProto\Db\Engines\DbInterface; use danog\MadelineProto\Db\DbPropertiesFabric; use danog\MadelineProto\Db\Mysql; -use danog\MadelineProto\Db\Types\ArrayType; use danog\MadelineProto\Loop\Generic\PeriodicLoop; use danog\MadelineProto\Loop\Update\FeedLoop; use danog\MadelineProto\Loop\Update\SeqLoop; @@ -286,6 +284,13 @@ class MTProto extends AsyncConstruct implements TLCallback * @var DbArray */ public $chats; + + /** + * Cache of usernames for chats + * + * @var DbArray + */ + public $usernames; /** * Cached parameters for fetching channel participants. * @@ -422,11 +427,7 @@ class MTProto extends AsyncConstruct implements TLCallback 'chats' => 'array', 'full_chats' => 'array', 'channel_participants' => 'array', - 'caching_simple' => 'array', - 'caching_simple_username' => 'array', - 'caching_possible_username' => 'array', - 'caching_full_info' => 'array', - 'caching_username_id' => 'array', + 'usernames' => 'array', ]; /** @@ -507,6 +508,7 @@ class MTProto extends AsyncConstruct implements TLCallback 'referenceDatabase', 'minDatabase', 'channel_participants', + 'usernames', // Misc caching 'dialog_params', @@ -569,18 +571,22 @@ class MTProto extends AsyncConstruct implements TLCallback if ($reset) { unset($this->{$property}); } else { - $this->{$property} = DbPropertiesFabric::get($this->settings['db'], $type, $property, $this->{$property}); + $this->{$property} = DbPropertiesFabric::get($this, $type, $property, $this->{$property}); } } - if (!$reset && count($this->caching_username_id) === 0) { - $this->logger('Filling database cache. This can take few minutes.', Logger::WARNING); - foreach ($this->chats as $id => $chat) { - if (isset($chat['username'])) { - $this->caching_username_id[$chat['username']] = $id; + if (!$reset && count($this->usernames) === 0) { + \Amp\Loop::run(function() { + $this->logger('Filling database cache. This can take few minutes.', Logger::WARNING); + $iterator = $this->chats->getIterator(); + while (yield $iterator->advance()) { + $chat = $iterator->getCurrent(); + if (isset($chat['username'])) { + $this->usernames->offsetSetAsync(\strtolower($chat['username']), $this->getId($chat)); + } } - } - $this->logger('Cache filled.', Logger::WARNING); + $this->logger('Cache filled.', Logger::WARNING); + }); } } @@ -1298,8 +1304,7 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Where internal database will be stored? * memory - session file - * sharedMemory - multiples instances share db if run in single process - * mysql - mysql database, shared by all instances in all processes. + * mysql - mysql database */ 'db' => [ 'type' => 'memory', diff --git a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php index cdb395e4..31f7f0e6 100644 --- a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php @@ -34,7 +34,6 @@ trait PeerHandler public $caching_simple_username = []; public $caching_possible_username = []; public $caching_full_info = []; - public $caching_username_id = []; /** * Convert MTProto channel ID to bot API channel ID. @@ -122,6 +121,7 @@ trait PeerHandler } } $this->chats[$user['id']] = $user; + $this->cacheChatUsername($user['id'], $user); $this->cachePwrChat($user['id'], false, true); } break; @@ -149,6 +149,7 @@ trait PeerHandler if (!isset($this->chats[-$chat['id']]) || $this->chats[-$chat['id']] !== $chat) { $this->logger->logger("Updated chat -{$chat['id']}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); $this->chats[-$chat['id']] = $chat; + $this->cacheChatUsername(-$chat['id'], $chat); $this->cachePwrChat(-$chat['id'], $this->settings['peer']['full_fetch'], true); } break; @@ -185,6 +186,7 @@ trait PeerHandler $chat = $newchat; } $this->chats[$bot_api_id] = $chat; + $this->cacheChatUsername($bot_api_id, $chat); if ($this->settings['peer']['full_fetch'] && (!isset($this->full_chats[$bot_api_id]) || $this->full_chats[$bot_api_id]['full']['participants_count'] !== (yield from $this->getFullInfo($bot_api_id))['full']['participants_count'])) { $this->cachePwrChat($bot_api_id, $this->settings['peer']['full_fetch'], true); } @@ -192,6 +194,14 @@ trait PeerHandler break; } } + + private function cacheChatUsername(int $id, array $chat) + { + if (!empty($chat['username'])) { + $this->usernames[strtolower($chat['username'])] = $id; + } + } + private function cachePwrChat($id, $full_fetch, $send) { \danog\MadelineProto\Tools::callFork((function () use ($id, $full_fetch, $send): \Generator { @@ -564,19 +574,12 @@ trait PeerHandler } return yield from $this->getInfo($this->supportUser); } - if ($bot_api_id = $this->caching_username_id[$id] ?? null) { + if ($bot_api_id = $this->usernames[$id] ?? null) { $chat = $this->chats[$bot_api_id]; - if (empty($chat['username']) || $chat['username'] !== $id) { - unset($this->caching_username_id[$id]); - } else { - return $this->genAll($this->chats[$bot_api_id], $folder_id); + if (empty($chat['username']) || \strtolower($chat['username']) !== $id) { + unset($this->usernames[$id]); } - } - foreach ($this->chats as $bot_api_id => $chat) { - if (isset($chat['username'])) { - $this->caching_username_id[$id] = $bot_api_id; - } if (isset($chat['username']) && \strtolower($chat['username']) === $id) { if ($chat['min'] ?? false && !isset($this->caching_full_info[$bot_api_id])) { $this->caching_full_info[$bot_api_id] = true; @@ -598,6 +601,7 @@ trait PeerHandler return $this->genAll($this->chats[$bot_api_id], $folder_id); } } + if ($recursive) { yield from $this->resolveUsername($id); return yield from $this->getInfo($id, false); From a883684f05f093ba48a62432aa28e47facdce6dc Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sat, 2 May 2020 20:36:59 +0300 Subject: [PATCH 06/25] Cache for Mysql Data Provider --- .../MadelineProto/Db/ArrayCacheTrait.php | 76 +++++++++++++++++++ src/danog/MadelineProto/Db/Mysql.php | 50 ++++++++---- src/danog/MadelineProto/Db/MysqlArray.php | 41 +++++----- src/danog/MadelineProto/MTProto.php | 3 +- 4 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 src/danog/MadelineProto/Db/ArrayCacheTrait.php diff --git a/src/danog/MadelineProto/Db/ArrayCacheTrait.php b/src/danog/MadelineProto/Db/ArrayCacheTrait.php new file mode 100644 index 00000000..27bb3f40 --- /dev/null +++ b/src/danog/MadelineProto/Db/ArrayCacheTrait.php @@ -0,0 +1,76 @@ + mixed, + * 'ttl' => int + * ], + * ... + * ] + * @var array + */ + protected array $cache = []; + protected string $ttl = '+1 day'; + private string $ttlCheckInterval = '+1 second'; + private int $nextTtlCheckTs = 0; + + protected function getCache(string $key, $default = null) + { + if ($cacheItem = $this->cache[$key] ?? null) { + $this->cache[$key]['ttl'] = strtotime($this->ttl); + } else { + return $default; + } + + return $cacheItem['value']; + } + + /** + * Save item in cache + * + * @param string $key + * @param $value + */ + protected function setCache(string $key, $value): void + { + $now = time(); + $this->cache[$key] = [ + 'value' => $value, + 'ttl' => $now, + ]; + + if ($this->nextTtlCheckTs < $now) { + $this->nextTtlCheckTs = strtotime($this->ttlCheckInterval, $now); + foreach ($this->cache as $cacheKey => $cacheValue) { + if ($cacheValue['ttl'] < $now) { + $this->unsetCache($cacheKey); + } + } + } + } + + /** + * Remove key from cache + * + * @param string $key + */ + protected function unsetCache(string $key): void + { + unset($this->cache[$key]); + } + + /** + * Remove all keys from cache + */ + protected function clearCache(): void + { + $this->cache = []; + } + +} \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php index 569cebe0..7f7f60cd 100644 --- a/src/danog/MadelineProto/Db/Mysql.php +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -5,26 +5,25 @@ namespace danog\MadelineProto\Db; use Amp\Mysql\ConnectionConfig; use Amp\Mysql\Pool; use function Amp\Mysql\Pool; +use function Amp\Promise\wait; class Mysql { /** @var Pool[] */ private static array $connections; - private static function connect( - string $host = '127.0.0.1', - int $port = 3306, - string $user = 'root', - string $password = '', - string $db = 'MadelineProto' - ) { - $config = ConnectionConfig::fromString( - "host={$host} port={$port} user={$user} password={$password} db={$db}" - ); - - return Pool($config); - } - + /** + * @param string $host + * @param int $port + * @param string $user + * @param string $password + * @param string $db + * + * @return Pool + * @throws \Amp\Sql\ConnectionException + * @throws \Amp\Sql\FailureException + * @throws \Throwable + */ public static function getConnection( string $host = '127.0.0.1', int $port = 3306, @@ -35,10 +34,31 @@ class Mysql { $dbKey = "$host:$port:$db"; if (empty(static::$connections[$dbKey])) { - static::$connections[$dbKey] = static::connect($host, $port, $user, $password, $db); + $config = ConnectionConfig::fromString( + "host={$host} port={$port} user={$user} password={$password} db={$db}" + ); + + static::createDb($config); + static::$connections[$dbKey] = pool($config); } return static::$connections[$dbKey]; } + /** + * @param ConnectionConfig $config + * + * @throws \Amp\Sql\ConnectionException + * @throws \Amp\Sql\FailureException + * @throws \Throwable + */ + private static function createDb(ConnectionConfig $config) { + $db = $config->getDatabase(); + wait(pool($config->withDatabase(null))->query(" + CREATE DATABASE IF NOT EXISTS `{$db}` + CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci + ")); + + } + } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index d9763efc..eccdc090 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -8,12 +8,13 @@ use Amp\Producer; use Amp\Promise; use Amp\Sql\ResultSet; use danog\MadelineProto\Logger; -use danog\MadelineProto\Tools; use function Amp\call; use function Amp\Promise\wait; class MysqlArray implements DbArray { + use ArrayCacheTrait; + private string $table; private array $settings; private Pool $db; @@ -48,6 +49,7 @@ class MysqlArray implements DbArray $instance->table = "{$tablePrefix}_{$name}"; $instance->settings = $settings; $instance->db = static::getDbConnection($settings); + $instance->ttl = $settings['cache_ttl'] ?? $instance->ttl; if ($value instanceof static) { if ($instance->table !== $value->table) { @@ -97,12 +99,7 @@ class MysqlArray implements DbArray */ public function offsetExists($index) { - $row = $this->syncRequest( - "SELECT count(`key`) as `count` FROM {$this->table} WHERE `key` = :index LIMIT 1", - ['index' => $index] - ); - - return !empty($row[0]['count']); + return $this->offsetGet($index) !== null; } /** @@ -126,11 +123,20 @@ class MysqlArray implements DbArray public function offsetGetAsync(string $offset): Promise { return call(function() use($offset) { + if ($cached = $this->getCache($offset)) { + return $cached; + } + $row = yield $this->request( "SELECT `value` FROM {$this->table} WHERE `key` = :index LIMIT 1", ['index' => $offset] ); - return $this->getValue($row); + + if ($value = $this->getValue($row)) { + $this->setCache($offset, $value); + } + + return $value; }); } @@ -149,20 +155,13 @@ class MysqlArray implements DbArray */ public function offsetSet($index, $value) { - $this->syncRequest(" - INSERT INTO `{$this->table}` - SET `key` = :index, `value` = :value - ON DUPLICATE KEY UPDATE `value` = :value - ", - [ - 'index' => $index, - 'value' => serialize($value), - ] - ); + wait($this->offsetSetAsync($index, $value)); } public function offsetSetAsync($index, $value): Promise { + $this->setCache($index, $value); + return $this->request(" INSERT INTO `{$this->table}` SET `key` = :index, `value` = :value @@ -189,6 +188,8 @@ class MysqlArray implements DbArray */ public function offsetUnset($index) { + $this->unsetCache($index); + $this->syncRequest(" DELETE FROM `{$this->table}` WHERE `key` = :index @@ -398,7 +399,7 @@ class MysqlArray implements DbArray */ private function syncRequest(string $query, array $params = []): array { - return Tools::wait($this->request($query, $params)); + return wait($this->request($query, $params)); } /** @@ -426,7 +427,5 @@ class MysqlArray implements DbArray } return $result; }); - - } } \ No newline at end of file diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 45aa8362..313bfc18 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -1314,7 +1314,8 @@ class MTProto extends AsyncConstruct implements TLCallback 'port' => 3306, 'user' => 'root', 'password' => '', - 'database' => 'MadelineProto' + 'database' => 'MadelineProto', + 'cache_ttl' => '+1 day', ] ], 'upload' => ['allow_automatic_upload' => true, 'part_size' => 512 * 1024, 'parallel_chunks' => 20], 'download' => ['report_broken_media' => true, 'part_size' => 1024 * 1024, 'parallel_chunks' => 20], 'pwr' => [ From 46f7e63734cc1248c38d52eb24c3e9a027845cf7 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 3 May 2020 04:33:54 +0300 Subject: [PATCH 07/25] Async refactor --- src/danog/MadelineProto/Db/DbArray.php | 11 +- .../MadelineProto/Db/DbPropertiesFabric.php | 15 +- src/danog/MadelineProto/Db/DbType.php | 2 +- src/danog/MadelineProto/Db/MemoryArray.php | 28 ++- src/danog/MadelineProto/Db/Mysql.php | 18 +- src/danog/MadelineProto/Db/MysqlArray.php | 168 ++++-------------- src/danog/MadelineProto/InternalDoc.php | 13 +- src/danog/MadelineProto/MTProto.php | 52 +++--- .../MTProtoTools/PeerHandler.php | 100 ++++++----- .../MTProtoTools/ReferenceDatabase.php | 8 +- .../MTProtoTools/UpdateHandler.php | 2 +- .../MadelineProto/Wrappers/DialogHandler.php | 6 +- src/danog/MadelineProto/Wrappers/Login.php | 2 +- 13 files changed, 188 insertions(+), 237 deletions(-) diff --git a/src/danog/MadelineProto/Db/DbArray.php b/src/danog/MadelineProto/Db/DbArray.php index 27b7e538..edfaf2b3 100644 --- a/src/danog/MadelineProto/Db/DbArray.php +++ b/src/danog/MadelineProto/Db/DbArray.php @@ -5,10 +5,13 @@ namespace danog\MadelineProto\Db; use Amp\Producer; use Amp\Promise; -interface DbArray extends DbType, \ArrayAccess, \Countable, \Iterator, \SeekableIterator +interface DbArray extends DbType, \ArrayAccess, \Countable { - public function getArrayCopy(); - public function offsetGetAsync(string $offset): Promise; - public function offsetSetAsync(string $offset, $value): Promise; + public function getArrayCopy(): array; + public function offsetExists($offset): Promise; + public function offsetGet($offset): Promise; + public function offsetSet($offset, $value); + public function offsetUnset($offset): Promise; + public function count(): Promise; public function getIterator(): Producer; } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/DbPropertiesFabric.php b/src/danog/MadelineProto/Db/DbPropertiesFabric.php index 6739ce3e..cc2dce8c 100644 --- a/src/danog/MadelineProto/Db/DbPropertiesFabric.php +++ b/src/danog/MadelineProto/Db/DbPropertiesFabric.php @@ -2,7 +2,6 @@ namespace danog\MadelineProto\Db; -use danog\MadelineProto\API; use danog\MadelineProto\MTProto; class DbPropertiesFabric @@ -13,7 +12,7 @@ class DbPropertiesFabric * @param string $name * @param $value * - * @return mixed + * @return DbType * * @uses \danog\MadelineProto\Db\MemoryArray * @uses \danog\MadelineProto\Db\SharedMemoryArray @@ -44,8 +43,18 @@ class DbPropertiesFabric throw new \InvalidArgumentException("Unknown $propertyType: {$propertyType}"); } - $prefix = (string) ($madelineProto->getSelf()['id'] ?? 'tmp'); + $prefix = static::getSessionId($madelineProto); return $class::getInstance($name, $value, $prefix, $dbSettings[$dbSettings['type']]??[]); } + private static function getSessionId(MTProto $madelineProto): string + { + $result = $madelineProto->getSelf()['id'] ?? null; + if (!$result) { + $result = 'tmp_'; + $result .= str_replace('0','', spl_object_hash($madelineProto)); + } + return (string) $result; + } + } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/DbType.php b/src/danog/MadelineProto/Db/DbType.php index bab9d584..f816a778 100644 --- a/src/danog/MadelineProto/Db/DbType.php +++ b/src/danog/MadelineProto/Db/DbType.php @@ -4,5 +4,5 @@ namespace danog\MadelineProto\Db; interface DbType { - static function getInstance(string $name, $value, string $tablePrefix, array $settings): self; + static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): self; } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MemoryArray.php b/src/danog/MadelineProto/Db/MemoryArray.php index 39d94ac1..18132189 100644 --- a/src/danog/MadelineProto/Db/MemoryArray.php +++ b/src/danog/MadelineProto/Db/MemoryArray.php @@ -13,7 +13,7 @@ class MemoryArray extends \ArrayIterator implements DbArray parent::__construct((array) $array, $flags | self::STD_PROP_LIST); } - public static function getInstance(string $name, $value, string $tablePrefix, array $settings): DbArray + public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): DbArray { if ($value instanceof DbArray) { $value = $value->getArrayCopy(); @@ -21,26 +21,36 @@ class MemoryArray extends \ArrayIterator implements DbArray return new static($value); } - public static function getDbConnection(array $settings) + public function offsetExists($offset): Promise { - return null; + return call(fn() => parent::offsetExists($offset)); } - public function offsetGetAsync(string $offset): Promise + public function offsetGet($offset): Promise { - return call(fn() => $this->offsetGet($offset)); + return call(fn() => parent::offsetGet($offset)); } - public function offsetSetAsync(string $offset, $value): Promise + public function offsetUnset($offset): Promise { - return call(fn() => $this->offsetSet($offset, $value)); + return call(fn() => parent::offsetUnset($offset)); + } + + public function count(): Promise + { + return call(fn() => parent::count()); + } + + public function getArrayCopy(): array + { + return parent::getArrayCopy(); } public function getIterator(): Producer { return new Producer(function (callable $emit) { - foreach ($this as $value) { - yield $emit($value); + foreach ($this as $key => $value) { + yield $emit([$key, $value]); } }); } diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php index 7f7f60cd..07c9051b 100644 --- a/src/danog/MadelineProto/Db/Mysql.php +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -4,6 +4,7 @@ namespace danog\MadelineProto\Db; use Amp\Mysql\ConnectionConfig; use Amp\Mysql\Pool; +use function Amp\call; use function Amp\Mysql\Pool; use function Amp\Promise\wait; @@ -52,12 +53,17 @@ class Mysql * @throws \Amp\Sql\FailureException * @throws \Throwable */ - private static function createDb(ConnectionConfig $config) { - $db = $config->getDatabase(); - wait(pool($config->withDatabase(null))->query(" - CREATE DATABASE IF NOT EXISTS `{$db}` - CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci - ")); + private static function createDb(ConnectionConfig $config) + { + wait(call(function() use($config) { + $db = $config->getDatabase(); + $connection = pool($config->withDatabase(null)); + yield $connection->query(" + CREATE DATABASE IF NOT EXISTS `{$db}` + CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci + "); + $connection->close(); + })); } diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index eccdc090..cbb3583e 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -18,8 +18,6 @@ class MysqlArray implements DbArray private string $table; private array $settings; private Pool $db; - private ?string $key = null; - private $current; public function __serialize(): array { @@ -42,7 +40,7 @@ class MysqlArray implements DbArray } - public static function getInstance(string $name, $value, string $tablePrefix, array $settings): DbType + public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): DbType { $instance = new static(); @@ -52,13 +50,18 @@ class MysqlArray implements DbArray $instance->ttl = $settings['cache_ttl'] ?? $instance->ttl; if ($value instanceof static) { - if ($instance->table !== $value->table) { + if ( + mb_strpos($value->table, 'tmp') === 0 && + mb_strpos($instance->table, 'tmp') !== 0 + ) { $instance->renameTable($value->table, $instance->table); + } elseif (mb_strpos($instance->table, 'tmp') === 0){ + $instance->table = $value->table; } } $instance->prepareTable(); - Loop::defer(function() use($value, $instance){ + Loop::defer(static function() use($value, $instance) { if (!empty($value) && !$value instanceof static) { Logger::log('Converting database.', Logger::ERROR); if ($value instanceof DbArray) { @@ -81,7 +84,6 @@ class MysqlArray implements DbArray } }); - return $instance; } @@ -94,33 +96,16 @@ class MysqlArray implements DbArray * The offset being checked. *

* - * @return bool true if the offset exists, otherwise false + * @return Promise true if the offset exists, otherwise false * @throws \Throwable */ - public function offsetExists($index) + public function offsetExists($index): Promise { - return $this->offsetGet($index) !== null; - } - - /** - * Get value for an offset - * - * @link https://php.net/manual/en/arrayiterator.offsetget.php - * - * @param string $index

- * The offset to get the value from. - *

- * - * @return mixed The value at offset index. - * @throws \Throwable - */ - public function offsetGet($index) - { - return wait($this->offsetGetAsync($index)); + return call(fn() => yield $this->offsetGet($index) !== null); } - public function offsetGetAsync(string $offset): Promise + public function offsetGet($offset): Promise { return call(function() use($offset) { if ($cached = $this->getCache($offset)) { @@ -150,19 +135,15 @@ class MysqlArray implements DbArray *

* @param $value * - * @return void + * @return Promise * @throws \Throwable */ - public function offsetSet($index, $value) - { - wait($this->offsetSetAsync($index, $value)); - } - public function offsetSetAsync($index, $value): Promise + public function offsetSet($index, $value): void { $this->setCache($index, $value); - return $this->request(" + $this->request(" INSERT INTO `{$this->table}` SET `key` = :index, `value` = :value ON DUPLICATE KEY UPDATE `value` = :value @@ -183,14 +164,14 @@ class MysqlArray implements DbArray * The offset to unset. *

* - * @return void + * @return Promise * @throws \Throwable */ - public function offsetUnset($index) + public function offsetUnset($index): Promise { $this->unsetCache($index); - $this->syncRequest(" + return $this->request(" DELETE FROM `{$this->table}` WHERE `key` = :index ", @@ -211,7 +192,7 @@ class MysqlArray implements DbArray $rows = $this->syncRequest("SELECT `key`, `value` FROM {$this->table}"); $result = []; foreach ($rows as $row) { - $result[$row['key']] = unserialize($row['value']); + $result[$row['key']] = $this->getValue($row); } return $result; @@ -225,7 +206,7 @@ class MysqlArray implements DbArray while (yield $request->advance()) { $row = $request->getCurrent(); - yield $emit($this->getValue($row)); + yield $emit([$row['key'], $this->getValue($row)]); } }); } @@ -234,40 +215,16 @@ class MysqlArray implements DbArray * Count elements * * @link https://php.net/manual/en/arrayiterator.count.php - * @return int The number of elements or public properties in the associated + * @return Promise The number of elements or public properties in the associated * array or object, respectively. * @throws \Throwable */ - public function count(): int + public function count(): Promise { - $row = $this->syncRequest("SELECT count(`key`) as `count` FROM {$this->table}"); - return $row[0]['count'] ?? 0; - } - - /** - * Rewind array back to the start - * - * @link https://php.net/manual/en/arrayiterator.rewind.php - * @return void - * @throws \Throwable - */ - public function rewind() - { - $this->key = null; - $this->key(); - $this->current = null; - } - - /** - * Return current array entry - * - * @link https://php.net/manual/en/arrayiterator.current.php - * @return mixed The current array entry. - * @throws \Throwable - */ - public function current() - { - return $this->current ?: $this->offsetGet($this->key()); + return call(function(){ + $row = yield $this->request("SELECT count(`key`) as `count` FROM {$this->table}"); + return $row[0]['count'] ?? 0; + }); } private function getValue(array $row) @@ -281,71 +238,6 @@ class MysqlArray implements DbArray return null; } - /** - * Return current array key - * - * @link https://php.net/manual/en/arrayiterator.key.php - * @return string|float|int|bool|null The current array key. - * @throws \Throwable - */ - public function key(): ?string - { - if ($this->key === null) { - $row = $this->syncRequest( - "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1" - ); - $this->key = $row[0]['key'] ?? null; - } - return $this->key; - } - - /** - * Move to next entry - * - * @link https://php.net/manual/en/arrayiterator.next.php - * @return void - * @throws \Throwable - */ - public function next() - { - $row = $this->syncRequest( - "SELECT `key`, `value` FROM {$this->table} WHERE `key` > :key ORDER BY `key` LIMIT 1", - ['key' => $this->key()] - ); - - $this->key = $row[0]['key'] ?? null; - $this->current = $this->getValue($row); - } - - /** - * Check whether array contains more entries - * - * @link https://php.net/manual/en/arrayiterator.valid.php - * @return bool - * @throws \Throwable - */ - public function valid():bool - { - return $this->key !== null; - } - - /** - * Seek to position - * @link https://php.net/manual/en/arrayiterator.seek.php - * @param int $position

- * The position to seek to. - *

- * @return void - */ - public function seek($position) - { - $row = $this->syncRequest( - "SELECT `key` FROM {$this->table} ORDER BY `key` LIMIT 1 OFFSET :position", - ['offset' => $position] - ); - $this->key = $row[0]['key'] ?? $this->key; - } - public static function getDbConnection(array $settings): Pool { return Mysql::getConnection( @@ -384,6 +276,14 @@ class MysqlArray implements DbArray "); } catch (\Throwable $e) { Logger::log("Cant rename table {$from} to {$to}", Logger::WARNING); + + try { + $this->syncRequest(" + DROP TABLE {$from}; + "); + } catch (\Throwable $e) { + Logger::log("Cant drop table {$from}", Logger::WARNING); + } } } diff --git a/src/danog/MadelineProto/InternalDoc.php b/src/danog/MadelineProto/InternalDoc.php index b14eac10..d49a4120 100644 --- a/src/danog/MadelineProto/InternalDoc.php +++ b/src/danog/MadelineProto/InternalDoc.php @@ -4285,11 +4285,12 @@ class InternalDoc extends APIFactory * * @param array $user User info * - * @return void + * @return \Generator + * @throws Exception */ - public function addUser(array $user): void + public function addUser(array $user): \Generator { - $this->API->addUser($user); + yield from $this->API->addUser($user); } /** * Call promise $b after promise $a. @@ -4754,11 +4755,11 @@ class InternalDoc extends APIFactory * * @param mixed $id Chat ID * - * @return integer + * @return \Generator */ - public function fullChatLastUpdated($id): int + public function fullChatLastUpdated($id): \Generator { - return $this->API->fullChatLastUpdated($id); + return yield from $this->API->fullChatLastUpdated($id); } /** * Get info about the logged-in user, not cached. diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 313bfc18..ab70a34d 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -22,6 +22,7 @@ namespace danog\MadelineProto; use Amp\Dns\Resolver; use Amp\File\StatCache; use Amp\Http\Client\HttpClient; +use Amp\Promise; use danog\MadelineProto\Async\AsyncConstruct; use danog\MadelineProto\Db\DbArray; use danog\MadelineProto\Db\DbPropertiesFabric; @@ -288,13 +289,13 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Cache of usernames for chats * - * @var DbArray + * @var DbArray|Promise[] */ public $usernames; /** * Cached parameters for fetching channel participants. * - * @var DbArray + * @var DbArray|Promise[] */ public $channel_participants; /** @@ -312,7 +313,7 @@ class MTProto extends AsyncConstruct implements TLCallback /** * Full chat info database. * - * @var DbArray + * @var DbArray|Promise[] */ public $full_chats; /** @@ -454,7 +455,7 @@ class MTProto extends AsyncConstruct implements TLCallback // Parse and store settings yield from $this->updateSettings($settings, false); $this->logger->logger(Lang::$current_lang['inst_dc'], Logger::ULTRA_VERBOSE); - $this->cleanupProperties(); + yield from $this->cleanupProperties(); // Load rsa keys $this->logger->logger(Lang::$current_lang['load_rsa'], Logger::ULTRA_VERBOSE); $this->rsa_keys = []; @@ -565,7 +566,7 @@ class MTProto extends AsyncConstruct implements TLCallback ]; } - public function initDb(bool $reset = false): void + public function initDb(bool $reset = false): \Generator { foreach ($this->dbProperies as $property => $type) { if ($reset) { @@ -575,18 +576,16 @@ class MTProto extends AsyncConstruct implements TLCallback } } - if (!$reset && count($this->usernames) === 0) { - \Amp\Loop::run(function() { - $this->logger('Filling database cache. This can take few minutes.', Logger::WARNING); - $iterator = $this->chats->getIterator(); - while (yield $iterator->advance()) { - $chat = $iterator->getCurrent(); - if (isset($chat['username'])) { - $this->usernames->offsetSetAsync(\strtolower($chat['username']), $this->getId($chat)); - } + if (!$reset && yield $this->usernames->count() === 0) { + $this->logger('Filling database cache. This can take few minutes.', Logger::WARNING); + $iterator = $this->chats->getIterator(); + while (yield $iterator->advance()) { + [$id, $chat] = $iterator->getCurrent(); + if (isset($chat['username'])) { + $this->usernames[\strtolower($chat['username'])] = $this->getId($chat); } - $this->logger('Cache filled.', Logger::WARNING); - }); + } + $this->logger('Cache filled.', Logger::WARNING); } } @@ -802,13 +801,17 @@ class MTProto extends AsyncConstruct implements TLCallback $this->TL->init($this->settings['tl_schema']['src'], $callbacks); } - $this->initDb(); + yield from $this->initDb(); } + /** * Upgrade MadelineProto instance. * * @return \Generator + * @throws Exception + * @throws RPCErrorException + * @throws \Throwable */ private function upgradeMadelineProto(): \Generator { @@ -831,16 +834,19 @@ class MTProto extends AsyncConstruct implements TLCallback unset($settings['authorization']['rsa_key']); } - $this->initDb(); + yield from $this->initDb(); if (!isset($this->secret_chats)) { $this->secret_chats = []; } - foreach ($this->full_chats as $id => $full) { + $iterator = $this->full_chats->getIterator(); + while (yield $iterator->advance()) { + [$id, $full] = $iterator->getCurrent(); if (isset($full['full'], $full['last_update'])) { $this->full_chats[$id] = ['full' => $full['full'], 'last_update' => $full['last_update']]; } } + foreach ($this->secret_chats as $key => &$chat) { if (!\is_array($chat)) { unset($this->secret_chats[$key]); @@ -942,7 +948,7 @@ class MTProto extends AsyncConstruct implements TLCallback $force = true; } // Cleanup old properties, init new stuffs - $this->cleanupProperties(); + yield from $this->cleanupProperties(); // Update TL callbacks $callbacks = [$this, $this->referenceDatabase]; if (!($this->authorization['user']['bot'] ?? false)) { @@ -1509,9 +1515,9 @@ class MTProto extends AsyncConstruct implements TLCallback * * @internal * - * @return void + * @return \Generator */ - public function resetSession(): void + public function resetSession(): \Generator { if (isset($this->seqUpdater)) { $this->seqUpdater->signal(true); @@ -1544,7 +1550,7 @@ class MTProto extends AsyncConstruct implements TLCallback $this->updates = []; $this->secret_chats = []; - $this->initDb(true); + yield from $this->initDb(true); $this->tos = ['expires' => 0, 'accepted' => true]; $this->referenceDatabase = new ReferenceDatabase($this); diff --git a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php index 31f7f0e6..60bcb749 100644 --- a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php @@ -22,6 +22,7 @@ namespace danog\MadelineProto\MTProtoTools; use Amp\Http\Client\Request; use danog\Decoder\FileId; use danog\Decoder\PhotoSizeSource\PhotoSizeSourceDialogPhoto; +use danog\MadelineProto\Db\DbArray; use const danog\Decoder\PROFILE_PHOTO; @@ -82,26 +83,29 @@ trait PeerHandler { $this->supportUser = $support['user']['id']; } + /** * Add user info. * * @param array $user User info * - * @return void + * @return \Generator + * @throws \danog\MadelineProto\Exception */ - public function addUser(array $user): void + public function addUser(array $user): \Generator { + $existingChat = yield $this->chats[$user['id']]; if (!isset($user['access_hash']) && !($user['min'] ?? false)) { - if (isset($this->chats[$user['id']]['access_hash']) && $this->chats[$user['id']]['access_hash']) { + if (!empty($existingChat['access_hash'])) { $this->logger->logger("No access hash with user {$user['id']}, using backup"); - $user['access_hash'] = $this->chats[$user['id']]['access_hash']; + $user['access_hash'] = $existingChat['access_hash']; } elseif (!isset($this->caching_simple[$user['id']]) && !(isset($user['username']) && isset($this->caching_simple_username[$user['username']]))) { $this->logger->logger("No access hash with user {$user['id']}, trying to fetch by ID..."); if (isset($user['username']) && !isset($this->caching_simple_username[$user['username']])) { $this->caching_possible_username[$user['id']] = $user['username']; } $this->cachePwrChat($user['id'], false, true); - } elseif (isset($user['username']) && !isset($this->chats[$user['id']]) && !isset($this->caching_simple_username[$user['username']])) { + } elseif (isset($user['username']) && !$existingChat && !isset($this->caching_simple_username[$user['username']])) { $this->logger->logger("No access hash with user {$user['id']}, trying to fetch by username..."); $this->cachePwrChat($user['username'], false, true); } else { @@ -111,13 +115,13 @@ trait PeerHandler } switch ($user['_']) { case 'user': - if (!isset($this->chats[$user['id']]) || $this->chats[$user['id']] !== $user) { + if (!$existingChat || $existingChat !== $user) { $this->logger->logger("Updated user {$user['id']}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); - if (($user['min'] ?? false) && isset($this->chats[$user['id']]) && !($this->chats[$user['id']]['min'] ?? false)) { + if (($user['min'] ?? false) && !($existingChat['min'] ?? false)) { $this->logger->logger("{$user['id']} is min, filling missing fields", \danog\MadelineProto\Logger::ULTRA_VERBOSE); - if (isset($this->chats[$user['id']]['access_hash'])) { + if (isset($existingChat['access_hash'])) { $user['min'] = false; - $user['access_hash'] = $this->chats[$user['id']]['access_hash']; + $user['access_hash'] = $existingChat['access_hash']; } } $this->chats[$user['id']] = $user; @@ -138,7 +142,7 @@ trait PeerHandler * * @internal * - * @return void + * @return \Generator */ public function addChat($chat): \Generator { @@ -146,7 +150,8 @@ trait PeerHandler case 'chat': case 'chatEmpty': case 'chatForbidden': - if (!isset($this->chats[-$chat['id']]) || $this->chats[-$chat['id']] !== $chat) { + $existingChat = yield $this->chats[-$chat['id']]; + if (!$existingChat || $existingChat !== $chat) { $this->logger->logger("Updated chat -{$chat['id']}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); $this->chats[-$chat['id']] = $chat; $this->cacheChatUsername(-$chat['id'], $chat); @@ -165,7 +170,7 @@ trait PeerHandler $this->caching_possible_username[$bot_api_id] = $chat['username']; } $this->cachePwrChat($bot_api_id, false, true); - } elseif (isset($chat['username']) && !isset($this->chats[$bot_api_id]) && !isset($this->caching_simple_username[$chat['username']])) { + } elseif (isset($chat['username']) && !(yield $this->chats[$bot_api_id]) && !isset($this->caching_simple_username[$chat['username']])) { $this->logger->logger("No access hash with {$chat['_']} {$bot_api_id}, trying to fetch by username..."); $this->cachePwrChat($chat['username'], false, true); } else { @@ -173,11 +178,12 @@ trait PeerHandler } return; } - if (!isset($this->chats[$bot_api_id]) || $this->chats[$bot_api_id] !== $chat) { + $existingChat = yield $this->chats[$bot_api_id]; + if (!$existingChat || $existingChat !== $chat) { $this->logger->logger("Updated chat {$bot_api_id}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); - if (($chat['min'] ?? false) && isset($this->chats[$bot_api_id]) && !($this->chats[$bot_api_id]['min'] ?? false)) { + if (($chat['min'] ?? false) && $existingChat && !($existingChat['min'] ?? false)) { $this->logger->logger("{$bot_api_id} is min, filling missing fields", \danog\MadelineProto\Logger::ULTRA_VERBOSE); - $newchat = $this->chats[$bot_api_id]; + $newchat = $existingChat; foreach (['title', 'username', 'photo', 'banned_rights', 'megagroup', 'verified'] as $field) { if (isset($chat[$field])) { $newchat[$field] = $chat[$field]; @@ -187,7 +193,8 @@ trait PeerHandler } $this->chats[$bot_api_id] = $chat; $this->cacheChatUsername($bot_api_id, $chat); - if ($this->settings['peer']['full_fetch'] && (!isset($this->full_chats[$bot_api_id]) || $this->full_chats[$bot_api_id]['full']['participants_count'] !== (yield from $this->getFullInfo($bot_api_id))['full']['participants_count'])) { + $fullChat = yield $this->full_chats[$bot_api_id]; + if ($this->settings['peer']['full_fetch'] && (!$fullChat || $fullChat['full']['participants_count'] !== (yield from $this->getFullInfo($bot_api_id))['full']['participants_count'])) { $this->cachePwrChat($bot_api_id, $this->settings['peer']['full_fetch'], true); } } @@ -224,7 +231,9 @@ trait PeerHandler public function peerIsset($id): \Generator { try { - return isset($this->chats[(yield from $this->getInfo($id))['bot_api_id']]); + $info = yield from $this->getInfo($id); + $chatId = $info['bot_api_id']; + return (yield $this->chats[$chatId]) !== null; } catch (\danog\MadelineProto\Exception $e) { return false; } catch (\danog\MadelineProto\RPCErrorException $e) { @@ -481,7 +490,7 @@ trait PeerHandler } $tried_simple = false; if (\is_numeric($id)) { - if (!isset($this->chats[$id])) { + if (! yield $this->chats[$id]) { try { $this->logger->logger("Try fetching {$id} with access hash 0"); $this->caching_simple[$id] = true; @@ -505,15 +514,15 @@ trait PeerHandler $tried_simple = true; } } - if (isset($this->chats[$id])) { - if (($this->chats[$id]['min'] ?? false) && $this->minDatabase->hasPeer($id) && !isset($this->caching_full_info[$id])) { + if (yield $this->chats[$id]) { + if (((yield $this->chats[$id])['min'] ?? false) && $this->minDatabase->hasPeer($id) && !isset($this->caching_full_info[$id])) { $this->caching_full_info[$id] = true; $this->logger->logger("Only have min peer for {$id} in database, trying to fetch full info"); try { if ($id < 0) { - yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll($this->chats[$id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); } else { - yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll($this->chats[$id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); @@ -524,10 +533,10 @@ trait PeerHandler } } try { - return $this->genAll($this->chats[$id], $folder_id); + return $this->genAll(yield $this->chats[$id], $folder_id); } catch (\danog\MadelineProto\Exception $e) { if ($e->getMessage() === 'This peer is not present in the internal peer database') { - unset($this->chats[$id]); + yield $this->chats->offsetUnset($id);/** @uses DbArray::offsetUnset() */ } else { throw $e; } @@ -574,10 +583,10 @@ trait PeerHandler } return yield from $this->getInfo($this->supportUser); } - if ($bot_api_id = $this->usernames[$id] ?? null) { - $chat = $this->chats[$bot_api_id]; + if ($bot_api_id = yield $this->usernames[$id]) { + $chat = yield $this->chats[$bot_api_id]; if (empty($chat['username']) || \strtolower($chat['username']) !== $id) { - unset($this->usernames[$id]); + yield $this->usernames->offsetUnset($id); /** @uses DbArray::offsetUnset() */ } if (isset($chat['username']) && \strtolower($chat['username']) === $id) { @@ -586,9 +595,9 @@ trait PeerHandler $this->logger->logger("Only have min peer for {$bot_api_id} in database, trying to fetch full info"); try { if ($bot_api_id < 0) { - yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll($this->chats[$bot_api_id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); } else { - yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll($this->chats[$bot_api_id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); + yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); @@ -598,7 +607,7 @@ trait PeerHandler unset($this->caching_full_info[$bot_api_id]); } } - return $this->genAll($this->chats[$bot_api_id], $folder_id); + return $this->genAll(yield $this->chats[$bot_api_id], $folder_id); } } @@ -674,11 +683,11 @@ trait PeerHandler * * @param mixed $id Chat ID * - * @return integer + * @return \Generator */ - public function fullChatLastUpdated($id): int + public function fullChatLastUpdated($id): \Generator { - return isset($this->full_chats[$id]['last_update']) ? $this->full_chats[$id]['last_update'] : 0; + return (yield $this->full_chats[$id])['last_update'] ?? 0; } /** * Get full info about peer, returns an FullInfo object. @@ -692,8 +701,8 @@ trait PeerHandler public function getFullInfo($id): \Generator { $partial = (yield from $this->getInfo($id)); - if (\time() - $this->fullChatLastUpdated($partial['bot_api_id']) < (isset($this->settings['peer']['full_info_cache_time']) ? $this->settings['peer']['full_info_cache_time'] : 0)) { - return \array_merge($partial, $this->full_chats[$partial['bot_api_id']]); + if (\time() - (yield from $this->fullChatLastUpdated($partial['bot_api_id'])) < (isset($this->settings['peer']['full_info_cache_time']) ? $this->settings['peer']['full_info_cache_time'] : 0)) { + return \array_merge($partial, yield $this->full_chats[$partial['bot_api_id']]); } switch ($partial['type']) { case 'user': @@ -912,10 +921,10 @@ trait PeerHandler } throw $e; } - if ($cached = $gres['_'] === 'channels.channelParticipantsNotModified') { - $gres = $this->fetchParticipantsCache($channel, $filter, $q, $offset, $limit); + if ($cached = ($gres['_'] === 'channels.channelParticipantsNotModified')) { + $gres = yield from $this->fetchParticipantsCache($channel, $filter, $q, $offset, $limit); } else { - $this->storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit); + yield from $this->storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit); } if ($last_count !== -1 && $last_count !== $gres['count']) { $has_more = true; @@ -975,11 +984,10 @@ trait PeerHandler } private function fetchParticipantsCache($channel, $filter, $q, $offset, $limit) { - return $this->channel_participants[$channel['channel_id']][$filter][$q][$offset][$limit]; + return (yield $this->channel_participants[$channel['channel_id']])[$filter][$q][$offset][$limit]; } - private function storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit) + private function storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit): \Generator { - //return; unset($gres['users']); $ids = []; foreach ($gres['participants'] as $participant) { @@ -987,13 +995,13 @@ trait PeerHandler } \sort($ids, SORT_NUMERIC); $gres['hash'] = \danog\MadelineProto\Tools::genVectorHash($ids); - $participant = $this->channel_participants[$channel['channel_id']]; + $participant = yield $this->channel_participants[$channel['channel_id']]; $participant[$filter][$q][$offset][$limit] = $gres; $this->channel_participants[$channel['channel_id']] = $participant; } private function getParticipantsHash($channel, $filter, $q, $offset, $limit) { - return isset($this->channel_participants[$channel['channel_id']][$filter][$q][$offset][$limit]) ? $this->channel_participants[$channel['channel_id']][$filter][$q][$offset][$limit]['hash'] : 0; + return (yield $this->channel_participants[$channel['channel_id']])[$filter][$q][$offset][$limit]['hash'] ?? 0; } private function storeDb($res, $force = false): \Generator { @@ -1054,6 +1062,12 @@ trait PeerHandler } } if ($res['_'] === 'contacts.resolvedPeer') { + foreach ($res['chats'] as $chat) { + yield from $this->addChat($chat); + } + foreach ($res['users'] as $user) { + yield from $this->addUser($user); + } return $res; } return false; diff --git a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php index 72dd357b..b877a57c 100644 --- a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php +++ b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php @@ -422,10 +422,10 @@ class ReferenceDatabase implements TLCallback break; // Peer + photo ID case self::PEER_PHOTO_ORIGIN: - if (isset($this->API->full_chats[$origin['peer']]['last_update'])) { - $chat = $this->API->full_chats[$origin['peer']]; - $chat['last_update'] = 0; - $this->API->full_chats[$origin['peer']] = $chat; + $fullChat = yield $this->API->full_chats[$origin['peer']]; + if (isset($fullChat['last_update'])) { + $fullChat['last_update'] = 0; + $this->API->full_chats[$origin['peer']] = $fullChat; } $this->API->getFullInfo($origin['peer']); break; diff --git a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php index facb7475..942e4d7c 100644 --- a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php @@ -332,7 +332,7 @@ trait UpdateHandler } if (\in_array($update['_'], ['updateUserName', 'updateUserPhone', 'updateUserBlocked', 'updateUserPhoto', 'updateContactRegistered', 'updateContactLink'])) { $id = $this->getId($update); - $chat = $this->full_chats[$id]; + $chat = yield $this->full_chats[$id]; $chat['last_update'] = 0; $this->full_chats[$id] = $chat; yield from $this->getFullInfo($id); diff --git a/src/danog/MadelineProto/Wrappers/DialogHandler.php b/src/danog/MadelineProto/Wrappers/DialogHandler.php index d7de6417..26f0eab1 100644 --- a/src/danog/MadelineProto/Wrappers/DialogHandler.php +++ b/src/danog/MadelineProto/Wrappers/DialogHandler.php @@ -32,13 +32,15 @@ trait DialogHandler { if ($this->authorization['user']['bot']) { $res = []; - foreach ($this->chats as $chat) { + /** @uses DbArray::getIterator() */ + $iterator = $this->chats->getIterator(); + while (yield $iterator->advance()) { + [$id, $chat] = $iterator->getCurrent(); try { $res[] = $this->genAll($chat)['Peer']; } catch (\Throwable $e) { continue; } - } return $res; } diff --git a/src/danog/MadelineProto/Wrappers/Login.php b/src/danog/MadelineProto/Wrappers/Login.php index 64809d66..82536ea9 100644 --- a/src/danog/MadelineProto/Wrappers/Login.php +++ b/src/danog/MadelineProto/Wrappers/Login.php @@ -35,7 +35,7 @@ trait Login public function logout(): \Generator { yield from $this->methodCallAsyncRead('auth.logOut', [], ['datacenter' => $this->datacenter->curdc]); - $this->resetSession(); + yield from $this->resetSession(); $this->logger->logger(\danog\MadelineProto\Lang::$current_lang['logout_ok'], \danog\MadelineProto\Logger::NOTICE); $this->startUpdateSystem(); return true; From 0c59e6ef4249cf9717405bab3e08a6a88b30fa4d Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 3 May 2020 16:16:20 +0300 Subject: [PATCH 08/25] Load submodules --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 0555de72..71ba0793 100644 --- a/composer.json +++ b/composer.json @@ -80,6 +80,9 @@ "url": "https://github.com/danog/dns" }], "scripts": { + "post-autoload-dump": [ + "git submodule init && git submodule update" + ], "build": [ "@docs", "@cs-fix", From ae9edd5512cd82a06ff10eea2d49e5acdb350894 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 3 May 2020 17:04:30 +0300 Subject: [PATCH 09/25] Mysql errors catch --- src/danog/MadelineProto/Db/MysqlArray.php | 33 +++++++++++------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index cbb3583e..33f62137 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -7,6 +7,7 @@ use Amp\Mysql\Pool; use Amp\Producer; use Amp\Promise; use Amp\Sql\ResultSet; +use danog\MadelineProto\Exception; use danog\MadelineProto\Logger; use function Amp\call; use function Amp\Promise\wait; @@ -49,7 +50,7 @@ class MysqlArray implements DbArray $instance->db = static::getDbConnection($settings); $instance->ttl = $settings['cache_ttl'] ?? $instance->ttl; - if ($value instanceof static) { + if ($value instanceof static && $value->table) { if ( mb_strpos($value->table, 'tmp') === 0 && mb_strpos($instance->table, 'tmp') !== 0 @@ -270,22 +271,12 @@ class MysqlArray implements DbArray private function renameTable(string $from, string $to) { - try { - $this->syncRequest(" - ALTER TABLE {$from} RENAME TO {$to}; - "); - } catch (\Throwable $e) { - Logger::log("Cant rename table {$from} to {$to}", Logger::WARNING); - - try { - $this->syncRequest(" - DROP TABLE {$from}; - "); - } catch (\Throwable $e) { - Logger::log("Cant drop table {$from}", Logger::WARNING); - } - } - + $this->syncRequest(" + ALTER TABLE {$from} RENAME TO {$to}; + "); + $this->syncRequest(" + DROP TABLE IF EXISTS {$from}; + "); } /** @@ -318,7 +309,13 @@ class MysqlArray implements DbArray return []; } - $request = yield $this->db->execute($query, $params); + try { + $request = yield $this->db->execute($query, $params); + } catch (\Throwable $e) { + Logger::log($e, Logger::ERROR); + return []; + } + $result = []; if ($request instanceof ResultSet) { while (yield $request->advance()) { From ab168ce6553758e40a587f0dd2b66fe37e9222fa Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Mon, 4 May 2020 23:09:39 +0300 Subject: [PATCH 10/25] Additional mysql settings --- src/danog/MadelineProto/Db/Mysql.php | 10 ++++++++-- src/danog/MadelineProto/Db/MysqlArray.php | 2 ++ src/danog/MadelineProto/MTProto.php | 6 ++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php index 07c9051b..736ada7f 100644 --- a/src/danog/MadelineProto/Db/Mysql.php +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -4,6 +4,7 @@ namespace danog\MadelineProto\Db; use Amp\Mysql\ConnectionConfig; use Amp\Mysql\Pool; +use Amp\Sql\Common\ConnectionPool; use function Amp\call; use function Amp\Mysql\Pool; use function Amp\Promise\wait; @@ -20,6 +21,9 @@ class Mysql * @param string $password * @param string $db * + * @param int $maxConnections + * @param int $idleTimeout + * * @return Pool * @throws \Amp\Sql\ConnectionException * @throws \Amp\Sql\FailureException @@ -30,7 +34,9 @@ class Mysql int $port = 3306, string $user = 'root', string $password = '', - string $db = 'MadelineProto' + string $db = 'MadelineProto', + int $maxConnections = ConnectionPool::DEFAULT_MAX_CONNECTIONS, + int $idleTimeout = ConnectionPool::DEFAULT_IDLE_TIMEOUT ): Pool { $dbKey = "$host:$port:$db"; @@ -40,7 +46,7 @@ class Mysql ); static::createDb($config); - static::$connections[$dbKey] = pool($config); + static::$connections[$dbKey] = pool($config, $maxConnections, $idleTimeout); } return static::$connections[$dbKey]; diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 33f62137..9bb5aa96 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -247,6 +247,8 @@ class MysqlArray implements DbArray $settings['user'], $settings['password'], $settings['database'], + $settings['max_connections'], + $settings['idle_timeout'] ); } diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index ab70a34d..d9e15291 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -1320,8 +1320,10 @@ class MTProto extends AsyncConstruct implements TLCallback 'port' => 3306, 'user' => 'root', 'password' => '', - 'database' => 'MadelineProto', - 'cache_ttl' => '+1 day', + 'database' => 'MadelineProto', //will be created automatically + 'max_connections' => 10, + 'idle_timeout' => 60, + 'cache_ttl' => '+1 day', //keep records in memory after last read ] ], 'upload' => ['allow_automatic_upload' => true, 'part_size' => 512 * 1024, 'parallel_chunks' => 20], 'download' => ['report_broken_media' => true, 'part_size' => 1024 * 1024, 'parallel_chunks' => 20], 'pwr' => [ From dedf20ea1bbc0e44063e51322fce8041318c7205 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Tue, 5 May 2020 20:05:32 +0300 Subject: [PATCH 11/25] Update create table statements. --- src/danog/MadelineProto/Db/Mysql.php | 5 +++-- src/danog/MadelineProto/Db/MysqlArray.php | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php index 736ada7f..593a94ab 100644 --- a/src/danog/MadelineProto/Db/Mysql.php +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -61,12 +61,13 @@ class Mysql */ private static function createDb(ConnectionConfig $config) { - wait(call(function() use($config) { + wait(call(static function() use($config) { $db = $config->getDatabase(); $connection = pool($config->withDatabase(null)); yield $connection->query(" CREATE DATABASE IF NOT EXISTS `{$db}` - CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci + CHARACTER SET 'utf8mb4' + COLLATE 'utf8mb4_general_ci' "); $connection->close(); })); diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 9bb5aa96..db048f46 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -268,6 +268,9 @@ class MysqlArray implements DbArray `ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`key`) ) + ENGINE = InnoDB + CHARACTER SET 'utf8mb4' + COLLATE 'utf8mb4_general_ci' "); } From 6cf1ef504f198f431774c47966c11e33b68a67c8 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sat, 9 May 2020 01:47:12 +0300 Subject: [PATCH 12/25] Always fill usernames cache. Cache fixes. --- .../MadelineProto/Db/ArrayCacheTrait.php | 35 ++++++++++++------- src/danog/MadelineProto/Db/MysqlArray.php | 3 ++ .../MTProtoTools/PeerHandler.php | 11 +++--- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/danog/MadelineProto/Db/ArrayCacheTrait.php b/src/danog/MadelineProto/Db/ArrayCacheTrait.php index 27bb3f40..dac22356 100644 --- a/src/danog/MadelineProto/Db/ArrayCacheTrait.php +++ b/src/danog/MadelineProto/Db/ArrayCacheTrait.php @@ -17,12 +17,15 @@ trait ArrayCacheTrait */ protected array $cache = []; protected string $ttl = '+1 day'; - private string $ttlCheckInterval = '+1 second'; + private string $ttlCheckInterval = '+1 minute'; private int $nextTtlCheckTs = 0; protected function getCache(string $key, $default = null) { - if ($cacheItem = $this->cache[$key] ?? null) { + $cacheItem = $this->cache[$key] ?? null; + $this->cleanupCache(); + + if ($cacheItem) { $this->cache[$key]['ttl'] = strtotime($this->ttl); } else { return $default; @@ -39,20 +42,12 @@ trait ArrayCacheTrait */ protected function setCache(string $key, $value): void { - $now = time(); $this->cache[$key] = [ 'value' => $value, - 'ttl' => $now, + 'ttl' => strtotime($this->ttl), ]; - if ($this->nextTtlCheckTs < $now) { - $this->nextTtlCheckTs = strtotime($this->ttlCheckInterval, $now); - foreach ($this->cache as $cacheKey => $cacheValue) { - if ($cacheValue['ttl'] < $now) { - $this->unsetCache($cacheKey); - } - } - } + $this->cleanupCache(); } /** @@ -73,4 +68,20 @@ trait ArrayCacheTrait $this->cache = []; } + /** + * Remove all keys from cache + */ + protected function cleanupCache(): void + { + $now = time(); + if ($this->nextTtlCheckTs < $now) { + $this->nextTtlCheckTs = strtotime($this->ttlCheckInterval, $now); + foreach ($this->cache as $cacheKey => $cacheValue) { + if ($cacheValue['ttl'] < $now) { + $this->unsetCache($cacheKey); + } + } + } + } + } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index db048f46..06db3c84 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -142,6 +142,9 @@ class MysqlArray implements DbArray public function offsetSet($index, $value): void { + if ($this->getCache($index) === $value) { + return; + } $this->setCache($index, $value); $this->request(" diff --git a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php index 60bcb749..3e3129bc 100644 --- a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php @@ -95,6 +95,9 @@ trait PeerHandler public function addUser(array $user): \Generator { $existingChat = yield $this->chats[$user['id']]; + if ($existingChat) { + $this->cacheChatUsername($user['id'], $user); + } if (!isset($user['access_hash']) && !($user['min'] ?? false)) { if (!empty($existingChat['access_hash'])) { $this->logger->logger("No access hash with user {$user['id']}, using backup"); @@ -125,9 +128,9 @@ trait PeerHandler } } $this->chats[$user['id']] = $user; - $this->cacheChatUsername($user['id'], $user); $this->cachePwrChat($user['id'], false, true); } + $this->cacheChatUsername($user['id'], $user); break; case 'userEmpty': break; @@ -154,9 +157,9 @@ trait PeerHandler if (!$existingChat || $existingChat !== $chat) { $this->logger->logger("Updated chat -{$chat['id']}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); $this->chats[-$chat['id']] = $chat; - $this->cacheChatUsername(-$chat['id'], $chat); $this->cachePwrChat(-$chat['id'], $this->settings['peer']['full_fetch'], true); } + $this->cacheChatUsername(-$chat['id'], $chat); break; case 'channelEmpty': break; @@ -192,19 +195,19 @@ trait PeerHandler $chat = $newchat; } $this->chats[$bot_api_id] = $chat; - $this->cacheChatUsername($bot_api_id, $chat); $fullChat = yield $this->full_chats[$bot_api_id]; if ($this->settings['peer']['full_fetch'] && (!$fullChat || $fullChat['full']['participants_count'] !== (yield from $this->getFullInfo($bot_api_id))['full']['participants_count'])) { $this->cachePwrChat($bot_api_id, $this->settings['peer']['full_fetch'], true); } } + $this->cacheChatUsername($bot_api_id, $chat); break; } } private function cacheChatUsername(int $id, array $chat) { - if (!empty($chat['username'])) { + if ($id && !empty($chat['username'])) { $this->usernames[strtolower($chat['username'])] = $id; } } From 8254360d2f52f6423b9bb1390fdd599aa39e19a5 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Mon, 11 May 2020 20:28:01 +0300 Subject: [PATCH 13/25] MemoryArray undefined offset fix --- src/danog/MadelineProto/Db/MemoryArray.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/danog/MadelineProto/Db/MemoryArray.php b/src/danog/MadelineProto/Db/MemoryArray.php index 18132189..5951a32a 100644 --- a/src/danog/MadelineProto/Db/MemoryArray.php +++ b/src/danog/MadelineProto/Db/MemoryArray.php @@ -28,7 +28,7 @@ class MemoryArray extends \ArrayIterator implements DbArray public function offsetGet($offset): Promise { - return call(fn() => parent::offsetGet($offset)); + return call(fn() => parent::offsetExists($offset) ? parent::offsetGet($offset) : null); } public function offsetUnset($offset): Promise From 912577617ec76a0a69330d8612b619d6d18c9729 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Tue, 12 May 2020 01:41:27 +0300 Subject: [PATCH 14/25] Mysql try ... catch --- src/danog/MadelineProto/Db/Mysql.php | 21 +++++++++++++-------- src/danog/MadelineProto/Db/MysqlArray.php | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/danog/MadelineProto/Db/Mysql.php b/src/danog/MadelineProto/Db/Mysql.php index 593a94ab..75d5618e 100644 --- a/src/danog/MadelineProto/Db/Mysql.php +++ b/src/danog/MadelineProto/Db/Mysql.php @@ -5,6 +5,7 @@ namespace danog\MadelineProto\Db; use Amp\Mysql\ConnectionConfig; use Amp\Mysql\Pool; use Amp\Sql\Common\ConnectionPool; +use danog\MadelineProto\Logger; use function Amp\call; use function Amp\Mysql\Pool; use function Amp\Promise\wait; @@ -62,14 +63,18 @@ class Mysql private static function createDb(ConnectionConfig $config) { wait(call(static function() use($config) { - $db = $config->getDatabase(); - $connection = pool($config->withDatabase(null)); - yield $connection->query(" - CREATE DATABASE IF NOT EXISTS `{$db}` - CHARACTER SET 'utf8mb4' - COLLATE 'utf8mb4_general_ci' - "); - $connection->close(); + try { + $db = $config->getDatabase(); + $connection = pool($config->withDatabase(null)); + yield $connection->query(" + CREATE DATABASE IF NOT EXISTS `{$db}` + CHARACTER SET 'utf8mb4' + COLLATE 'utf8mb4_general_ci' + "); + $connection->close(); + } catch (\Throwable $e) { + Logger::log($e->getMessage(), Logger::ERROR); + } })); } diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 06db3c84..6452629f 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -320,7 +320,7 @@ class MysqlArray implements DbArray try { $request = yield $this->db->execute($query, $params); } catch (\Throwable $e) { - Logger::log($e, Logger::ERROR); + Logger::log($e->getMessage(), Logger::ERROR); return []; } From 123e3fadfdcbfaf842fba0abe27cb44174b7be4b Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Tue, 12 May 2020 15:49:40 +0300 Subject: [PATCH 15/25] Remove unavailable method call --- src/danog/MadelineProto/Db/MysqlArray.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 6452629f..ab267516 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -74,10 +74,10 @@ class MysqlArray implements DbArray foreach ((array) $value as $key => $item) { $counter++; if ($counter % 100 === 0) { - yield $instance->offsetSetAsync($key, $item); + yield from $instance->offsetSet($key, $item); Logger::log("Converting database. $counter/$total", Logger::WARNING); } else { - $instance->offsetSetAsync($key, $item); + $instance->offsetSet($key, $item); } } @@ -136,18 +136,18 @@ class MysqlArray implements DbArray *

* @param $value * - * @return Promise + * @return void * @throws \Throwable */ - public function offsetSet($index, $value): void + public function offsetSet($index, $value) { if ($this->getCache($index) === $value) { return; } $this->setCache($index, $value); - $this->request(" + yield $this->request(" INSERT INTO `{$this->table}` SET `key` = :index, `value` = :value ON DUPLICATE KEY UPDATE `value` = :value From fa1d73009dbf6fde3762ed9c37e775250c6871e1 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Tue, 12 May 2020 20:35:15 +0300 Subject: [PATCH 16/25] Add more async requests --- src/danog/MadelineProto/Db/MysqlArray.php | 48 ++++++++++++----------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index ab267516..021c470d 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -7,7 +7,6 @@ use Amp\Mysql\Pool; use Amp\Producer; use Amp\Promise; use Amp\Sql\ResultSet; -use danog\MadelineProto\Exception; use danog\MadelineProto\Logger; use function Amp\call; use function Amp\Promise\wait; @@ -50,19 +49,20 @@ class MysqlArray implements DbArray $instance->db = static::getDbConnection($settings); $instance->ttl = $settings['cache_ttl'] ?? $instance->ttl; - if ($value instanceof static && $value->table) { - if ( - mb_strpos($value->table, 'tmp') === 0 && - mb_strpos($instance->table, 'tmp') !== 0 - ) { - $instance->renameTable($value->table, $instance->table); - } elseif (mb_strpos($instance->table, 'tmp') === 0){ - $instance->table = $value->table; - } - } - $instance->prepareTable(); - Loop::defer(static function() use($value, $instance) { + if ($value instanceof static && $value->table) { + if ( + mb_strpos($value->table, 'tmp') === 0 && + mb_strpos($instance->table, 'tmp') !== 0 + ) { + yield from $instance->renameTable($value->table, $instance->table); + } elseif (mb_strpos($instance->table, 'tmp') === 0){ + $instance->table = $value->table; + } + } + + yield from $instance->prepareTable(); + if (!empty($value) && !$value instanceof static) { Logger::log('Converting database.', Logger::ERROR); if ($value instanceof DbArray) { @@ -74,7 +74,7 @@ class MysqlArray implements DbArray foreach ((array) $value as $key => $item) { $counter++; if ($counter % 100 === 0) { - yield from $instance->offsetSet($key, $item); + yield $instance->offsetSet($key, $item); Logger::log("Converting database. $counter/$total", Logger::WARNING); } else { $instance->offsetSet($key, $item); @@ -136,18 +136,17 @@ class MysqlArray implements DbArray *

* @param $value * - * @return void * @throws \Throwable */ - public function offsetSet($index, $value) + public function offsetSet($index, $value): Promise { if ($this->getCache($index) === $value) { - return; + return call(fn()=>null); } $this->setCache($index, $value); - yield $this->request(" + return $this->request(" INSERT INTO `{$this->table}` SET `key` = :index, `value` = :value ON DUPLICATE KEY UPDATE `value` = :value @@ -263,7 +262,7 @@ class MysqlArray implements DbArray */ private function prepareTable() { - return $this->syncRequest(" + return yield $this->request(" CREATE TABLE IF NOT EXISTS `{$this->table}` ( `key` VARCHAR(255) NOT NULL, @@ -279,10 +278,11 @@ class MysqlArray implements DbArray private function renameTable(string $from, string $to) { - $this->syncRequest(" + yield $this->request(" ALTER TABLE {$from} RENAME TO {$to}; "); - $this->syncRequest(" + + yield $this->request(" DROP TABLE IF EXISTS {$from}; "); } @@ -313,7 +313,11 @@ class MysqlArray implements DbArray private function request(string $query, array $params = []): Promise { return call(function() use($query, $params) { - if (empty($this->db)) { + + Logger::log([$query, $params], Logger::VERBOSE); + + if (empty($this->db) || !$this->db->isAlive()) { + Logger::log('No database connection', Logger::WARNING); return []; } From 9016a7c0dd88ed6049e25874918ddf7a696b4767 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 17 May 2020 00:38:30 +0300 Subject: [PATCH 17/25] Allow dots in mysql table names --- src/danog/MadelineProto/Db/MysqlArray.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 021c470d..565bc080 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -114,7 +114,7 @@ class MysqlArray implements DbArray } $row = yield $this->request( - "SELECT `value` FROM {$this->table} WHERE `key` = :index LIMIT 1", + "SELECT `value` FROM `{$this->table}` WHERE `key` = :index LIMIT 1", ['index' => $offset] ); @@ -192,7 +192,7 @@ class MysqlArray implements DbArray */ public function getArrayCopy(): array { - $rows = $this->syncRequest("SELECT `key`, `value` FROM {$this->table}"); + $rows = $this->syncRequest("SELECT `key`, `value` FROM `{$this->table}`"); $result = []; foreach ($rows as $row) { $result[$row['key']] = $this->getValue($row); @@ -204,7 +204,7 @@ class MysqlArray implements DbArray public function getIterator(): Producer { return new Producer(function (callable $emit) { - $request = yield $this->db->execute("SELECT `key`, `value` FROM {$this->table}"); + $request = yield $this->db->execute("SELECT `key`, `value` FROM `{$this->table}`"); while (yield $request->advance()) { $row = $request->getCurrent(); @@ -225,7 +225,7 @@ class MysqlArray implements DbArray public function count(): Promise { return call(function(){ - $row = yield $this->request("SELECT count(`key`) as `count` FROM {$this->table}"); + $row = yield $this->request("SELECT count(`key`) as `count` FROM `{$this->table}`"); return $row[0]['count'] ?? 0; }); } @@ -279,11 +279,11 @@ class MysqlArray implements DbArray private function renameTable(string $from, string $to) { yield $this->request(" - ALTER TABLE {$from} RENAME TO {$to}; + ALTER TABLE `{$from}` RENAME TO `{$to}`; "); yield $this->request(" - DROP TABLE IF EXISTS {$from}; + DROP TABLE IF EXISTS `{$from}`; "); } From fea71efc99a738d6300372699f74935fd37a161a Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 17 May 2020 22:04:32 +0300 Subject: [PATCH 18/25] Upgrade for MysqlArray initialization --- .../MadelineProto/Db/DbPropertiesFabric.php | 5 +- src/danog/MadelineProto/Db/DbType.php | 12 ++- src/danog/MadelineProto/Db/MemoryArray.php | 12 +-- src/danog/MadelineProto/Db/MysqlArray.php | 78 +++++++++++-------- src/danog/MadelineProto/MTProto.php | 2 +- 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/src/danog/MadelineProto/Db/DbPropertiesFabric.php b/src/danog/MadelineProto/Db/DbPropertiesFabric.php index cc2dce8c..40c75d92 100644 --- a/src/danog/MadelineProto/Db/DbPropertiesFabric.php +++ b/src/danog/MadelineProto/Db/DbPropertiesFabric.php @@ -2,6 +2,7 @@ namespace danog\MadelineProto\Db; +use Amp\Promise; use danog\MadelineProto\MTProto; class DbPropertiesFabric @@ -12,13 +13,13 @@ class DbPropertiesFabric * @param string $name * @param $value * - * @return DbType + * @return Promise * * @uses \danog\MadelineProto\Db\MemoryArray * @uses \danog\MadelineProto\Db\SharedMemoryArray * @uses \danog\MadelineProto\Db\MysqlArray */ - public static function get(MTProto $madelineProto, string $propertyType, string $name, $value = null): DbType + public static function get(MTProto $madelineProto, string $propertyType, string $name, $value = null): Promise { $class = __NAMESPACE__; $dbSettings = $madelineProto->settings['db']; diff --git a/src/danog/MadelineProto/Db/DbType.php b/src/danog/MadelineProto/Db/DbType.php index f816a778..19f15e04 100644 --- a/src/danog/MadelineProto/Db/DbType.php +++ b/src/danog/MadelineProto/Db/DbType.php @@ -2,7 +2,17 @@ namespace danog\MadelineProto\Db; +use Amp\Promise; + interface DbType { - static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): self; + /** + * @param string $name + * @param null $value + * @param string $tablePrefix + * @param array $settings + * + * @return Promise + */ + static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): Promise; } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MemoryArray.php b/src/danog/MadelineProto/Db/MemoryArray.php index 5951a32a..a7f65584 100644 --- a/src/danog/MadelineProto/Db/MemoryArray.php +++ b/src/danog/MadelineProto/Db/MemoryArray.php @@ -13,12 +13,14 @@ class MemoryArray extends \ArrayIterator implements DbArray parent::__construct((array) $array, $flags | self::STD_PROP_LIST); } - public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): DbArray + public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): Promise { - if ($value instanceof DbArray) { - $value = $value->getArrayCopy(); - } - return new static($value); + return call(function() use ($value) { + if ($value instanceof DbArray) { + $value = $value->getArrayCopy(); + } + return new static($value); + }); } public function offsetExists($offset): Promise diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 565bc080..85c14e00 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -40,7 +40,7 @@ class MysqlArray implements DbArray } - public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): DbType + public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): Promise { $instance = new static(); @@ -49,43 +49,51 @@ class MysqlArray implements DbArray $instance->db = static::getDbConnection($settings); $instance->ttl = $settings['cache_ttl'] ?? $instance->ttl; - Loop::defer(static function() use($value, $instance) { - if ($value instanceof static && $value->table) { - if ( - mb_strpos($value->table, 'tmp') === 0 && - mb_strpos($instance->table, 'tmp') !== 0 - ) { - yield from $instance->renameTable($value->table, $instance->table); - } elseif (mb_strpos($instance->table, 'tmp') === 0){ - $instance->table = $value->table; - } - } - + return call(static function() use($instance, $value) { + yield from static::renameTmpTable($instance, $value); yield from $instance->prepareTable(); + Loop::defer(fn() => static::migrateDataToDb($instance, $value)); - if (!empty($value) && !$value instanceof static) { - Logger::log('Converting database.', Logger::ERROR); - if ($value instanceof DbArray) { - $value = $value->getArrayCopy(); - } - $value = (array) $value; - $counter = 0; - $total = count($value); - foreach ((array) $value as $key => $item) { - $counter++; - if ($counter % 100 === 0) { - yield $instance->offsetSet($key, $item); - Logger::log("Converting database. $counter/$total", Logger::WARNING); - } else { - $instance->offsetSet($key, $item); - } - - } - Logger::log('Converting database done.', Logger::ERROR); - } + return $instance; }); + } - return $instance; + private static function renameTmpTable(MysqlArray $instance, ?DbArray $value): \Generator + { + if ($value instanceof static && $value->table) { + if ( + mb_strpos($value->table, 'tmp') === 0 && + mb_strpos($instance->table, 'tmp') !== 0 + ) { + yield from $instance->renameTable($value->table, $instance->table); + } elseif (mb_strpos($instance->table, 'tmp') === 0) { + $instance->table = $value->table; + } + } + } + + private static function migrateDataToDb(MysqlArray $instance, ?DbArray $value): \Generator + { + if (!empty($value) && !$value instanceof static) { + Logger::log('Converting database.', Logger::ERROR); + if ($value instanceof DbArray) { + $value = $value->getArrayCopy(); + } + $value = (array) $value; + $counter = 0; + $total = count($value); + foreach ((array) $value as $key => $item) { + $counter++; + if ($counter % 100 === 0) { + yield $instance->offsetSet($key, $item); + Logger::log("Converting database. $counter/$total", Logger::WARNING); + } else { + $instance->offsetSet($key, $item); + } + + } + Logger::log('Converting database done.', Logger::ERROR); + } } /** @@ -262,6 +270,7 @@ class MysqlArray implements DbArray */ private function prepareTable() { + Logger::log("Creating/checking table {$this->table}", Logger::WARNING); return yield $this->request(" CREATE TABLE IF NOT EXISTS `{$this->table}` ( @@ -278,6 +287,7 @@ class MysqlArray implements DbArray private function renameTable(string $from, string $to) { + Logger::log("Renaming table {$from} to {$to}", Logger::WARNING); yield $this->request(" ALTER TABLE `{$from}` RENAME TO `{$to}`; "); diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index d9e15291..e71ad4fe 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -572,7 +572,7 @@ class MTProto extends AsyncConstruct implements TLCallback if ($reset) { unset($this->{$property}); } else { - $this->{$property} = DbPropertiesFabric::get($this, $type, $property, $this->{$property}); + $this->{$property} = yield DbPropertiesFabric::get($this, $type, $property, $this->{$property}); } } From d0e57f2535be021621032d7c63592e2e4f32c8ed Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 17 May 2020 22:04:58 +0300 Subject: [PATCH 19/25] Decrease default cache_ttl --- src/danog/MadelineProto/Db/ArrayCacheTrait.php | 2 +- src/danog/MadelineProto/MTProto.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/danog/MadelineProto/Db/ArrayCacheTrait.php b/src/danog/MadelineProto/Db/ArrayCacheTrait.php index dac22356..66ed3ad7 100644 --- a/src/danog/MadelineProto/Db/ArrayCacheTrait.php +++ b/src/danog/MadelineProto/Db/ArrayCacheTrait.php @@ -16,7 +16,7 @@ trait ArrayCacheTrait * @var array */ protected array $cache = []; - protected string $ttl = '+1 day'; + protected string $ttl = '+5 minutes'; private string $ttlCheckInterval = '+1 minute'; private int $nextTtlCheckTs = 0; diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index e71ad4fe..8656aaa9 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -1323,7 +1323,7 @@ class MTProto extends AsyncConstruct implements TLCallback 'database' => 'MadelineProto', //will be created automatically 'max_connections' => 10, 'idle_timeout' => 60, - 'cache_ttl' => '+1 day', //keep records in memory after last read + 'cache_ttl' => '+5 minutes', //keep records in memory after last read ] ], 'upload' => ['allow_automatic_upload' => true, 'part_size' => 512 * 1024, 'parallel_chunks' => 20], 'download' => ['report_broken_media' => true, 'part_size' => 1024 * 1024, 'parallel_chunks' => 20], 'pwr' => [ From 7c1b602779328877a6f988167f298370e0260f81 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Mon, 18 May 2020 18:50:47 +0300 Subject: [PATCH 20/25] Fix Undefined index: value in ArrayCacheTrait.php:34 --- .../MadelineProto/Db/ArrayCacheTrait.php | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/danog/MadelineProto/Db/ArrayCacheTrait.php b/src/danog/MadelineProto/Db/ArrayCacheTrait.php index 66ed3ad7..f94a90eb 100644 --- a/src/danog/MadelineProto/Db/ArrayCacheTrait.php +++ b/src/danog/MadelineProto/Db/ArrayCacheTrait.php @@ -23,15 +23,16 @@ trait ArrayCacheTrait protected function getCache(string $key, $default = null) { $cacheItem = $this->cache[$key] ?? null; - $this->cleanupCache(); + $result = $default; - if ($cacheItem) { + if (\is_array($cacheItem)) { + $result = $cacheItem['value']; $this->cache[$key]['ttl'] = strtotime($this->ttl); - } else { - return $default; } - return $cacheItem['value']; + $this->cleanupCache(); + + return $result; } /** @@ -74,14 +75,20 @@ trait ArrayCacheTrait protected function cleanupCache(): void { $now = time(); - if ($this->nextTtlCheckTs < $now) { - $this->nextTtlCheckTs = strtotime($this->ttlCheckInterval, $now); - foreach ($this->cache as $cacheKey => $cacheValue) { - if ($cacheValue['ttl'] < $now) { - $this->unsetCache($cacheKey); - } + if ($this->nextTtlCheckTs > $now) { + return; + } + + $this->nextTtlCheckTs = strtotime($this->ttlCheckInterval, $now); + $oldKeys = []; + foreach ($this->cache as $cacheKey => $cacheValue) { + if ($cacheValue['ttl'] < $now) { + $oldKeys[] = $cacheKey; } } + foreach ($oldKeys as $oldKey) { + $this->unsetCache($oldKey); + } } } \ No newline at end of file From e0832390ce83bdf7707c2f2a9f722cb557543af2 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Wed, 20 May 2020 23:15:52 +0300 Subject: [PATCH 21/25] Type fixes --- src/danog/MadelineProto/Db/MysqlArray.php | 29 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 85c14e00..801a680b 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -40,6 +40,14 @@ class MysqlArray implements DbArray } + /** + * @param string $name + * @param DbArray|array|null $value + * @param string $tablePrefix + * @param array $settings + * + * @return Promise + */ public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): Promise { $instance = new static(); @@ -58,7 +66,13 @@ class MysqlArray implements DbArray }); } - private static function renameTmpTable(MysqlArray $instance, ?DbArray $value): \Generator + /** + * @param MysqlArray $instance + * @param DbArray|array|null $value + * + * @return \Generator + */ + private static function renameTmpTable(MysqlArray $instance, $value): \Generator { if ($value instanceof static && $value->table) { if ( @@ -72,13 +86,18 @@ class MysqlArray implements DbArray } } - private static function migrateDataToDb(MysqlArray $instance, ?DbArray $value): \Generator + /** + * @param MysqlArray $instance + * @param DbArray|array|null $value + * + * @return \Generator + * @throws \Throwable + */ + private static function migrateDataToDb(MysqlArray $instance, $value): \Generator { if (!empty($value) && !$value instanceof static) { Logger::log('Converting database.', Logger::ERROR); - if ($value instanceof DbArray) { - $value = $value->getArrayCopy(); - } + $value = (array) $value; $counter = 0; $total = count($value); From 1e23970ab3e4413bb79a10885482c38ee2f3a28c Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 7 Jun 2020 01:00:44 +0300 Subject: [PATCH 22/25] DbArray refactoring and convertation improvement --- src/danog/MadelineProto/Db/DbArray.php | 2 +- .../MadelineProto/Db/DbPropertiesFabric.php | 21 ++----- .../MadelineProto/Db/DbPropertiesTrait.php | 52 +++++++++++++++++ src/danog/MadelineProto/Db/MemoryArray.php | 13 +++-- src/danog/MadelineProto/Db/MysqlArray.php | 57 ++++++++----------- src/danog/MadelineProto/MTProto.php | 33 ++--------- 6 files changed, 96 insertions(+), 82 deletions(-) create mode 100644 src/danog/MadelineProto/Db/DbPropertiesTrait.php diff --git a/src/danog/MadelineProto/Db/DbArray.php b/src/danog/MadelineProto/Db/DbArray.php index edfaf2b3..b7d68586 100644 --- a/src/danog/MadelineProto/Db/DbArray.php +++ b/src/danog/MadelineProto/Db/DbArray.php @@ -7,7 +7,7 @@ use Amp\Promise; interface DbArray extends DbType, \ArrayAccess, \Countable { - public function getArrayCopy(): array; + public function getArrayCopy(): Promise; public function offsetExists($offset): Promise; public function offsetGet($offset): Promise; public function offsetSet($offset, $value); diff --git a/src/danog/MadelineProto/Db/DbPropertiesFabric.php b/src/danog/MadelineProto/Db/DbPropertiesFabric.php index 40c75d92..7e85f5db 100644 --- a/src/danog/MadelineProto/Db/DbPropertiesFabric.php +++ b/src/danog/MadelineProto/Db/DbPropertiesFabric.php @@ -3,12 +3,12 @@ namespace danog\MadelineProto\Db; use Amp\Promise; -use danog\MadelineProto\MTProto; class DbPropertiesFabric { /** - * @param MTProto $madelineProto + * @param array $dbSettings + * @param string $namePrefix * @param string $propertyType * @param string $name * @param $value @@ -19,10 +19,10 @@ class DbPropertiesFabric * @uses \danog\MadelineProto\Db\SharedMemoryArray * @uses \danog\MadelineProto\Db\MysqlArray */ - public static function get(MTProto $madelineProto, string $propertyType, string $name, $value = null): Promise + public static function get(array $dbSettings, string $namePrefix, string $propertyType, string $name, $value = null): Promise { $class = __NAMESPACE__; - $dbSettings = $madelineProto->settings['db']; + switch (strtolower($dbSettings['type'])) { case 'memory': $class .= '\Memory'; @@ -44,18 +44,7 @@ class DbPropertiesFabric throw new \InvalidArgumentException("Unknown $propertyType: {$propertyType}"); } - $prefix = static::getSessionId($madelineProto); - return $class::getInstance($name, $value, $prefix, $dbSettings[$dbSettings['type']]??[]); - } - - private static function getSessionId(MTProto $madelineProto): string - { - $result = $madelineProto->getSelf()['id'] ?? null; - if (!$result) { - $result = 'tmp_'; - $result .= str_replace('0','', spl_object_hash($madelineProto)); - } - return (string) $result; + return $class::getInstance($name, $value, $namePrefix, $dbSettings[$dbSettings['type']]??[]); } } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/DbPropertiesTrait.php b/src/danog/MadelineProto/Db/DbPropertiesTrait.php new file mode 100644 index 00000000..4c5a6f33 --- /dev/null +++ b/src/danog/MadelineProto/Db/DbPropertiesTrait.php @@ -0,0 +1,52 @@ +dbProperies)) { + throw new \LogicException(__CLASS__ . ' must have a $dbProperies'); + } + $dbSettings = $MadelineProto->settings['db']; + $prefix = static::getSessionId($MadelineProto); + + foreach ($this->dbProperies as $property => $type) { + if ($reset) { + unset($this->{$property}); + } else { + $this->{$property} = yield DbPropertiesFabric::get($dbSettings, $prefix, $type, $property, $this->{$property}); + } + } + + if (!$reset && yield $this->usernames->count() === 0) { + $this->logger('Filling database cache. This can take few minutes.', Logger::WARNING); + $iterator = $this->chats->getIterator(); + while (yield $iterator->advance()) { + [$id, $chat] = $iterator->getCurrent(); + if (isset($chat['username'])) { + $this->usernames[\strtolower($chat['username'])] = $this->getId($chat); + } + } + $this->logger('Cache filled.', Logger::WARNING); + } + } + + private static function getSessionId(MTProto $madelineProto): string + { + $result = $madelineProto->getSelf()['id'] ?? null; + if (!$result) { + $result = 'tmp_'; + $result .= str_replace('0','', spl_object_hash($madelineProto)); + } + + $className = explode('\\',__CLASS__); + $result .= '_' . end($className); + return $result; + } +} \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MemoryArray.php b/src/danog/MadelineProto/Db/MemoryArray.php index a7f65584..24f319c3 100644 --- a/src/danog/MadelineProto/Db/MemoryArray.php +++ b/src/danog/MadelineProto/Db/MemoryArray.php @@ -4,6 +4,7 @@ namespace danog\MadelineProto\Db; use Amp\Producer; use Amp\Promise; +use danog\MadelineProto\Logger; use function Amp\call; class MemoryArray extends \ArrayIterator implements DbArray @@ -15,9 +16,13 @@ class MemoryArray extends \ArrayIterator implements DbArray public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): Promise { - return call(function() use ($value) { + return call(static function() use ($value) { + if ($value instanceof MemoryArray) { + return $value; + } if ($value instanceof DbArray) { - $value = $value->getArrayCopy(); + Logger::log("Loading database to memory. Please wait.", Logger::WARNING); + $value = yield $value->getArrayCopy(); } return new static($value); }); @@ -43,9 +48,9 @@ class MemoryArray extends \ArrayIterator implements DbArray return call(fn() => parent::count()); } - public function getArrayCopy(): array + public function getArrayCopy(): Promise { - return parent::getArrayCopy(); + return call(fn() => parent::getArrayCopy()); } public function getIterator(): Producer diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 801a680b..8c842a7a 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -60,7 +60,7 @@ class MysqlArray implements DbArray return call(static function() use($instance, $value) { yield from static::renameTmpTable($instance, $value); yield from $instance->prepareTable(); - Loop::defer(fn() => static::migrateDataToDb($instance, $value)); + yield from static::migrateDataToDb($instance, $value); return $instance; }); @@ -76,11 +76,11 @@ class MysqlArray implements DbArray { if ($value instanceof static && $value->table) { if ( - mb_strpos($value->table, 'tmp') === 0 && + $value->table !== $instance->table && mb_strpos($instance->table, 'tmp') !== 0 ) { yield from $instance->renameTable($value->table, $instance->table); - } elseif (mb_strpos($instance->table, 'tmp') === 0) { + } else { $instance->table = $value->table; } } @@ -95,17 +95,21 @@ class MysqlArray implements DbArray */ private static function migrateDataToDb(MysqlArray $instance, $value): \Generator { - if (!empty($value) && !$value instanceof static) { + if (!empty($value) && !$value instanceof MysqlArray) { Logger::log('Converting database.', Logger::ERROR); - $value = (array) $value; + if ($value instanceof DbArray) { + $value = yield $value->getArrayCopy(); + } else { + $value = (array) $value; + } $counter = 0; $total = count($value); - foreach ((array) $value as $key => $item) { + foreach ($value as $key => $item) { $counter++; if ($counter % 100 === 0) { yield $instance->offsetSet($key, $item); - Logger::log("Converting database. $counter/$total", Logger::WARNING); + Logger::log("Loading data to table {$instance->table}: $counter/$total", Logger::WARNING); } else { $instance->offsetSet($key, $item); } @@ -212,20 +216,20 @@ class MysqlArray implements DbArray /** * Get array copy * - * @link https://php.net/manual/en/arrayiterator.getarraycopy.php - * @return array A copy of the array, or array of public properties - * if ArrayIterator refers to an object. + * @return Promise * @throws \Throwable */ - public function getArrayCopy(): array + public function getArrayCopy(): Promise { - $rows = $this->syncRequest("SELECT `key`, `value` FROM `{$this->table}`"); - $result = []; - foreach ($rows as $row) { - $result[$row['key']] = $this->getValue($row); - } - - return $result; + return call(function(){ + $iterator = $this->getIterator(); + $result = []; + while (yield $iterator->advance()) { + [$key, $value] = $iterator->getCurrent(); + $result[$key] = $value; + } + return $result; + }); } public function getIterator(): Producer @@ -235,7 +239,6 @@ class MysqlArray implements DbArray while (yield $request->advance()) { $row = $request->getCurrent(); - yield $emit([$row['key'], $this->getValue($row)]); } }); @@ -317,21 +320,7 @@ class MysqlArray implements DbArray } /** - * Perform blocking request to db - * - * @param string $query - * @param array $params - * - * @return array|null - * @throws \Throwable - */ - private function syncRequest(string $query, array $params = []): array - { - return wait($this->request($query, $params)); - } - - /** - * Perform blocking request to db + * Perform async request to db * * @param string $query * @param array $params diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 8656aaa9..aeb69538 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -26,6 +26,7 @@ use Amp\Promise; use danog\MadelineProto\Async\AsyncConstruct; use danog\MadelineProto\Db\DbArray; use danog\MadelineProto\Db\DbPropertiesFabric; +use danog\MadelineProto\Db\DbPropertiesTrait; use danog\MadelineProto\Db\Mysql; use danog\MadelineProto\Loop\Generic\PeriodicLoop; use danog\MadelineProto\Loop\Update\FeedLoop; @@ -72,6 +73,7 @@ class MTProto extends AsyncConstruct implements TLCallback use \danog\MadelineProto\Wrappers\Start; use \danog\MadelineProto\Wrappers\Templates; use \danog\MadelineProto\Wrappers\TOS; + use DbPropertiesTrait; /** * Old internal version of MadelineProto. * @@ -424,7 +426,7 @@ class MTProto extends AsyncConstruct implements TLCallback * @see DbPropertiesFabric * @var array */ - private array $dbProperies = [ + protected array $dbProperies = [ 'chats' => 'array', 'full_chats' => 'array', 'channel_participants' => 'array', @@ -566,29 +568,6 @@ class MTProto extends AsyncConstruct implements TLCallback ]; } - public function initDb(bool $reset = false): \Generator - { - foreach ($this->dbProperies as $property => $type) { - if ($reset) { - unset($this->{$property}); - } else { - $this->{$property} = yield DbPropertiesFabric::get($this, $type, $property, $this->{$property}); - } - } - - if (!$reset && yield $this->usernames->count() === 0) { - $this->logger('Filling database cache. This can take few minutes.', Logger::WARNING); - $iterator = $this->chats->getIterator(); - while (yield $iterator->advance()) { - [$id, $chat] = $iterator->getCurrent(); - if (isset($chat['username'])) { - $this->usernames[\strtolower($chat['username'])] = $this->getId($chat); - } - } - $this->logger('Cache filled.', Logger::WARNING); - } - } - /** * Cleanup memory and session file. * @@ -801,7 +780,7 @@ class MTProto extends AsyncConstruct implements TLCallback $this->TL->init($this->settings['tl_schema']['src'], $callbacks); } - yield from $this->initDb(); + yield from $this->initDb($this); } @@ -834,7 +813,7 @@ class MTProto extends AsyncConstruct implements TLCallback unset($settings['authorization']['rsa_key']); } - yield from $this->initDb(); + yield from $this->initDb($this); if (!isset($this->secret_chats)) { $this->secret_chats = []; @@ -1552,7 +1531,7 @@ class MTProto extends AsyncConstruct implements TLCallback $this->updates = []; $this->secret_chats = []; - yield from $this->initDb(true); + yield from $this->initDb($this,true); $this->tos = ['expires' => 0, 'accepted' => true]; $this->referenceDatabase = new ReferenceDatabase($this); From 2aab5e8cc8e02666d8089b48434dbc8580b0916d Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Sun, 7 Jun 2020 21:56:54 +0300 Subject: [PATCH 23/25] getParticipantsHash generator fix --- src/danog/MadelineProto/MTProtoTools/PeerHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php index 3e3129bc..461c67d4 100644 --- a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php @@ -916,7 +916,7 @@ trait PeerHandler $last_count = -1; do { try { - $gres = yield from $this->methodCallAsyncRead('channels.getParticipants', ['channel' => $channel, 'filter' => ['_' => $filter, 'q' => $q], 'offset' => $offset, 'limit' => $limit, 'hash' => $hash = $this->getParticipantsHash($channel, $filter, $q, $offset, $limit)], ['datacenter' => $this->datacenter->curdc, 'heavy' => true]); + $gres = yield from $this->methodCallAsyncRead('channels.getParticipants', ['channel' => $channel, 'filter' => ['_' => $filter, 'q' => $q], 'offset' => $offset, 'limit' => $limit, 'hash' => $hash = yield from $this->getParticipantsHash($channel, $filter, $q, $offset, $limit)], ['datacenter' => $this->datacenter->curdc, 'heavy' => true]); } catch (\danog\MadelineProto\RPCErrorException $e) { if ($e->rpc === 'CHAT_ADMIN_REQUIRED') { $this->logger->logger($e->rpc); @@ -1002,7 +1002,7 @@ trait PeerHandler $participant[$filter][$q][$offset][$limit] = $gres; $this->channel_participants[$channel['channel_id']] = $participant; } - private function getParticipantsHash($channel, $filter, $q, $offset, $limit) + private function getParticipantsHash($channel, $filter, $q, $offset, $limit): \Generator { return (yield $this->channel_participants[$channel['channel_id']])[$filter][$q][$offset][$limit]['hash'] ?? 0; } From ca03bc662a87727a8388b65a132b82c06d4ea58b Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Tue, 9 Jun 2020 01:47:54 +0300 Subject: [PATCH 24/25] New mysql cache cleanup --- .../MadelineProto/Db/ArrayCacheTrait.php | 28 +++++++++---------- src/danog/MadelineProto/Db/MysqlArray.php | 2 ++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/danog/MadelineProto/Db/ArrayCacheTrait.php b/src/danog/MadelineProto/Db/ArrayCacheTrait.php index f94a90eb..20abdb9c 100644 --- a/src/danog/MadelineProto/Db/ArrayCacheTrait.php +++ b/src/danog/MadelineProto/Db/ArrayCacheTrait.php @@ -2,6 +2,9 @@ namespace danog\MadelineProto\Db; +use Amp\Loop; +use danog\MadelineProto\Logger; + trait ArrayCacheTrait { /** @@ -18,7 +21,6 @@ trait ArrayCacheTrait protected array $cache = []; protected string $ttl = '+5 minutes'; private string $ttlCheckInterval = '+1 minute'; - private int $nextTtlCheckTs = 0; protected function getCache(string $key, $default = null) { @@ -30,8 +32,6 @@ trait ArrayCacheTrait $this->cache[$key]['ttl'] = strtotime($this->ttl); } - $this->cleanupCache(); - return $result; } @@ -47,8 +47,6 @@ trait ArrayCacheTrait 'value' => $value, 'ttl' => strtotime($this->ttl), ]; - - $this->cleanupCache(); } /** @@ -61,12 +59,9 @@ trait ArrayCacheTrait unset($this->cache[$key]); } - /** - * Remove all keys from cache - */ - protected function clearCache(): void + protected function startCacheCleanupLoop(): void { - $this->cache = []; + Loop::repeat(strtotime($this->ttlCheckInterval, 0) * 1000, fn() => $this->cleanupCache()); } /** @@ -75,11 +70,6 @@ trait ArrayCacheTrait protected function cleanupCache(): void { $now = time(); - if ($this->nextTtlCheckTs > $now) { - return; - } - - $this->nextTtlCheckTs = strtotime($this->ttlCheckInterval, $now); $oldKeys = []; foreach ($this->cache as $cacheKey => $cacheValue) { if ($cacheValue['ttl'] < $now) { @@ -89,6 +79,14 @@ trait ArrayCacheTrait foreach ($oldKeys as $oldKey) { $this->unsetCache($oldKey); } + + Logger::log( + sprintf( + "cache for table:%s; keys left: %s; keys removed: %s", + $this->table, \count($this->cache), \count($oldKeys) + ), + Logger::VERBOSE + ); } } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 8c842a7a..143e84b2 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -57,6 +57,8 @@ class MysqlArray implements DbArray $instance->db = static::getDbConnection($settings); $instance->ttl = $settings['cache_ttl'] ?? $instance->ttl; + $instance->startCacheCleanupLoop(); + return call(static function() use($instance, $value) { yield from static::renameTmpTable($instance, $value); yield from $instance->prepareTable(); From 321787f718364b8f40165188e3019816cf0b8382 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Thu, 11 Jun 2020 00:23:35 +0300 Subject: [PATCH 25/25] Disable isset, optimize getInstance, concurrent offsetSet --- src/danog/MadelineProto/Db/DbArray.php | 13 ++++- src/danog/MadelineProto/Db/MemoryArray.php | 9 +++- src/danog/MadelineProto/Db/MysqlArray.php | 55 +++++++++++++--------- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/danog/MadelineProto/Db/DbArray.php b/src/danog/MadelineProto/Db/DbArray.php index b7d68586..b86385d8 100644 --- a/src/danog/MadelineProto/Db/DbArray.php +++ b/src/danog/MadelineProto/Db/DbArray.php @@ -8,10 +8,21 @@ use Amp\Promise; interface DbArray extends DbType, \ArrayAccess, \Countable { public function getArrayCopy(): Promise; - public function offsetExists($offset): Promise; + public function isset($key): Promise; public function offsetGet($offset): Promise; public function offsetSet($offset, $value); public function offsetUnset($offset): Promise; public function count(): Promise; public function getIterator(): Producer; + + /** + * @deprecated + * @internal + * @see DbArray::isset(); + * + * @param mixed $offset + * + * @return bool + */ + public function offsetExists($offset); } \ No newline at end of file diff --git a/src/danog/MadelineProto/Db/MemoryArray.php b/src/danog/MadelineProto/Db/MemoryArray.php index 24f319c3..8a97116f 100644 --- a/src/danog/MadelineProto/Db/MemoryArray.php +++ b/src/danog/MadelineProto/Db/MemoryArray.php @@ -28,9 +28,14 @@ class MemoryArray extends \ArrayIterator implements DbArray }); } - public function offsetExists($offset): Promise + public function offsetExists($offset) { - return call(fn() => parent::offsetExists($offset)); + throw new \RuntimeException('Native isset not support promises. Use isset method'); + } + + public function isset($key): Promise + { + return call(fn() => parent::offsetExists($key)); } public function offsetGet($offset): Promise diff --git a/src/danog/MadelineProto/Db/MysqlArray.php b/src/danog/MadelineProto/Db/MysqlArray.php index 143e84b2..e29e4657 100644 --- a/src/danog/MadelineProto/Db/MysqlArray.php +++ b/src/danog/MadelineProto/Db/MysqlArray.php @@ -2,14 +2,12 @@ namespace danog\MadelineProto\Db; -use Amp\Loop; use Amp\Mysql\Pool; use Amp\Producer; use Amp\Promise; use Amp\Sql\ResultSet; use danog\MadelineProto\Logger; use function Amp\call; -use function Amp\Promise\wait; class MysqlArray implements DbArray { @@ -50,9 +48,14 @@ class MysqlArray implements DbArray */ public static function getInstance(string $name, $value = null, string $tablePrefix = '', array $settings = []): Promise { - $instance = new static(); + $tableName = "{$tablePrefix}_{$name}"; + if ($value instanceof self && $value->table === $tableName) { + $instance = &$value; + } else { + $instance = new static(); + $instance->table = $tableName; + } - $instance->table = "{$tablePrefix}_{$name}"; $instance->settings = $settings; $instance->db = static::getDbConnection($settings); $instance->ttl = $settings['cache_ttl'] ?? $instance->ttl; @@ -60,9 +63,13 @@ class MysqlArray implements DbArray $instance->startCacheCleanupLoop(); return call(static function() use($instance, $value) { - yield from static::renameTmpTable($instance, $value); yield from $instance->prepareTable(); - yield from static::migrateDataToDb($instance, $value); + + //Skip migrations if its same object + if ($instance !== $value) { + yield from static::renameTmpTable($instance, $value); + yield from static::migrateDataToDb($instance, $value); + } return $instance; }); @@ -109,7 +116,7 @@ class MysqlArray implements DbArray $total = count($value); foreach ($value as $key => $item) { $counter++; - if ($counter % 100 === 0) { + if ($counter % 500 === 0) { yield $instance->offsetSet($key, $item); Logger::log("Loading data to table {$instance->table}: $counter/$total", Logger::WARNING); } else { @@ -121,21 +128,21 @@ class MysqlArray implements DbArray } } + public function offsetExists($index): bool + { + throw new \RuntimeException('Native isset not support promises. Use isset method'); + } + /** - * Check if offset exists + * Check if key isset * - * @link https://php.net/manual/en/arrayiterator.offsetexists.php - * - * @param string $index

- * The offset being checked. - *

+ * @param $key * * @return Promise true if the offset exists, otherwise false - * @throws \Throwable */ - public function offsetExists($index): Promise + public function isset($key): Promise { - return call(fn() => yield $this->offsetGet($index) !== null); + return call(fn() => yield $this->offsetGet($key) !== null); } @@ -177,18 +184,24 @@ class MysqlArray implements DbArray if ($this->getCache($index) === $value) { return call(fn()=>null); } + $this->setCache($index, $value); - return $this->request(" - INSERT INTO `{$this->table}` - SET `key` = :index, `value` = :value - ON DUPLICATE KEY UPDATE `value` = :value - ", + $request = $this->request(" + INSERT INTO `{$this->table}` + SET `key` = :index, `value` = :value + ON DUPLICATE KEY UPDATE `value` = :value + ", [ 'index' => $index, 'value' => serialize($value), ] ); + + //Ensure that cache is synced with latest insert in case of concurrent requests. + $request->onResolve(fn() => $this->setCache($index, $value)); + + return $request; } /**