From: Stanislav Malyshev Date: Thu, 9 Jun 2016 19:38:05 +0000 (-0700) Subject: Create BagOStuff implementation to talk to RestBase X-Git-Tag: 1.31.0-rc.0~6446 X-Git-Url: http://git.cyclocoop.org/%28?a=commitdiff_plain;h=7ade0a7c678ffa4233728683a91c2aeab0c8d344;p=lhc%2Fweb%2Fwiklou.git Create BagOStuff implementation to talk to RestBase Or any other HTTP REST server. Bug: T137272 Change-Id: Iefef24ffa831ba59d7725da8d20d5addb544b3ab --- diff --git a/autoload.php b/autoload.php index d99401623d..0211c6d076 100644 --- a/autoload.php +++ b/autoload.php @@ -1092,6 +1092,7 @@ $wgAutoloadLocalClasses = [ 'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php', 'RCFeedEngine' => __DIR__ . '/includes/rcfeed/RCFeedEngine.php', 'RCFeedFormatter' => __DIR__ . '/includes/rcfeed/RCFeedFormatter.php', + 'RESTBagOStuff' => __DIR__ . '/includes/libs/objectcache/RESTBagOStuff.php', 'RSSFeed' => __DIR__ . '/includes/Feed.php', 'RandomPage' => __DIR__ . '/includes/specials/SpecialRandompage.php', 'RangeDifference' => __DIR__ . '/includes/diff/DiffEngine.php', diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 331f2d5e99..0371f24296 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -104,7 +104,7 @@ class MultiHttpClient { * - reqTimeout : post-connection timeout per request (seconds) * @return array Response array for request */ - final public function run( array $req, array $opts = [] ) { + public function run( array $req, array $opts = [] ) { return $this->runMulti( [ $req ], $opts )[0]['response']; } diff --git a/includes/libs/objectcache/RESTBagOStuff.php b/includes/libs/objectcache/RESTBagOStuff.php new file mode 100644 index 0000000000..1bbfc56c19 --- /dev/null +++ b/includes/libs/objectcache/RESTBagOStuff.php @@ -0,0 +1,127 @@ + 'RESTBagOStuff', + * 'url' => 'http://localhost:7231/wikimedia.org/v1/sessions/' + * ); + * @endcode + */ +class RESTBagOStuff extends BagOStuff { + + /** + * @var MultiHttpClient + */ + private $client; + + /** + * REST URL to use for storage. + * @var string + */ + private $url; + + public function __construct( $params ) { + if ( empty( $params['url'] ) ) { + throw new InvalidArgumentException( 'URL parameter is required' ); + } + parent::__construct( $params ); + if ( empty( $params['client'] ) ) { + $this->client = new MultiHttpClient( [] ); + } else { + $this->client = $params['client']; + } + // Make sure URL ends with / + $this->url = rtrim( $params['url'], '/' ) . '/'; + } + + /** + * @param string $key + * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional] + * @return mixed Returns false on failure and if the item does not exist + */ + protected function doGet( $key, $flags = 0 ) { + $req = [ + 'method' => 'GET', + 'url' => $this->url . rawurlencode( $key ), + + ]; + list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->client->run( $req ); + if ( $rcode === 200 ) { + if ( is_string( $rbody ) ) { + return unserialize( $rbody ); + } + return false; + } + if ( $rcode === 0 || ( $rcode >= 400 && $rcode != 404 ) ) { + return $this->handleError( "Failed to fetch $key", $rcode, $rerr ); + } + return false; + } + + /** + * Handle storage error + * @param string $msg Error message + * @param int $rcode Error code from client + * @param string $rerr Error message from client + * @return false + */ + protected function handleError( $msg, $rcode, $rerr ) { + $this->logger->error( "$msg : ({code}) {error}", [ + 'code' => $rcode, + 'error' => $rerr + ] ); + $this->setLastError( $rcode === 0 ? self::ERR_UNREACHABLE : self::ERR_UNEXPECTED ); + return false; + } + + /** + * Set an item + * + * @param string $key + * @param mixed $value + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @param int $flags Bitfield of BagOStuff::WRITE_* constants + * @return bool Success + */ + public function set( $key, $value, $exptime = 0, $flags = 0 ) { + $req = [ + 'method' => 'PUT', + 'url' => $this->url . rawurlencode( $key ), + 'body' => serialize( $value ) + ]; + list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->client->run( $req ); + if ( $rcode === 200 || $rcode === 201 ) { + return true; + } + return $this->handleError( "Failed to store $key", $rcode, $rerr ); + } + + /** + * Delete an item. + * + * @param string $key + * @return bool True if the item was deleted or not found, false on failure + */ + public function delete( $key ) { + $req = [ + 'method' => 'DELETE', + 'url' => $this->url . rawurlencode( $key ), + ]; + list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->client->run( $req ); + if ( $rcode === 200 || $rcode === 204 || $rcode === 205 ) { + return true; + } + return $this->handleError( "Failed to delete $key", $rcode, $rerr ); + } +} diff --git a/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php b/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php new file mode 100644 index 0000000000..ebeb1092a6 --- /dev/null +++ b/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php @@ -0,0 +1,88 @@ +client = + $this->getMockBuilder( 'MultiHttpClient' ) + ->setConstructorArgs( [ [] ] ) + ->setMethods( [ 'run' ] ) + ->getMock(); + $this->bag = new RESTBagOStuff( [ 'client' => $this->client, 'url' => 'http://test/rest/' ] ); + } + + public function testGet() { + $this->client->expects( $this->once() )->method( 'run' )->with( [ + 'method' => 'GET', + 'url' => 'http://test/rest/42xyz42' + // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) + ] )->willReturn( [ 200, 'OK', [], 's:8:"somedata";', 0 ] ); + $result = $this->bag->get( '42xyz42' ); + $this->assertEquals( 'somedata', $result ); + } + + public function testGetNotExist() { + $this->client->expects( $this->once() )->method( 'run' )->with( [ + 'method' => 'GET', + 'url' => 'http://test/rest/42xyz42' + // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) + ] )->willReturn( [ 404, 'Not found', [], 'Nothing to see here', 0 ] ); + $result = $this->bag->get( '42xyz42' ); + $this->assertFalse( $result ); + } + + public function testGetBadClient() { + $this->client->expects( $this->once() )->method( 'run' )->with( [ + 'method' => 'GET', + 'url' => 'http://test/rest/42xyz42' + // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) + ] )->willReturn( [ 0, '', [], '', 'cURL has failed you today' ] ); + $result = $this->bag->get( '42xyz42' ); + $this->assertFalse( $result ); + $this->assertEquals( BagOStuff::ERR_UNREACHABLE, $this->bag->getLastError() ); + } + + public function testGetBadServer() { + $this->client->expects( $this->once() )->method( 'run' )->with( [ + 'method' => 'GET', + 'url' => 'http://test/rest/42xyz42' + // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) + ] )->willReturn( [ 500, 'Too busy', [], 'Server is too busy', '' ] ); + $result = $this->bag->get( '42xyz42' ); + $this->assertFalse( $result ); + $this->assertEquals( BagOStuff::ERR_UNEXPECTED, $this->bag->getLastError() ); + } + + public function testPut() { + $this->client->expects( $this->once() )->method( 'run' )->with( [ + 'method' => 'PUT', + 'url' => 'http://test/rest/42xyz42', + 'body' => 's:8:"postdata";' + // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) + ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] ); + $result = $this->bag->set( '42xyz42', 'postdata' ); + $this->assertTrue( $result ); + } + + public function testDelete() { + $this->client->expects( $this->once() )->method( 'run' )->with( [ + 'method' => 'DELETE', + 'url' => 'http://test/rest/42xyz42', + // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) + ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] ); + $result = $this->bag->delete( '42xyz42' ); + $this->assertTrue( $result ); + } +}