<?php
+use Psr\Log\LoggerInterface;
+
/**
* Interface to key-value storage behind an HTTP server.
*
* @endcode
*/
class RESTBagOStuff extends BagOStuff {
+ /**
+ * Default connection timeout in seconds. The kernel retransmits the SYN
+ * packet after 1 second, so 1.2 seconds allows for 1 retransmit without
+ * permanent failure.
+ */
+ const DEFAULT_CONN_TIMEOUT = 1.2;
+
+ /**
+ * Default request timeout
+ */
+ const DEFAULT_REQ_TIMEOUT = 3.0;
/**
* @var MultiHttpClient
if ( empty( $params['url'] ) ) {
throw new InvalidArgumentException( 'URL parameter is required' );
}
- parent::__construct( $params );
if ( empty( $params['client'] ) ) {
- $this->client = new MultiHttpClient( [] );
+ // Pass through some params to the HTTP client.
+ $clientParams = [
+ 'connTimeout' => $params['connTimeout'] ?? self::DEFAULT_CONN_TIMEOUT,
+ 'reqTimeout' => $params['reqTimeout'] ?? self::DEFAULT_REQ_TIMEOUT,
+ ];
+ foreach ( [ 'caBundlePath', 'proxy' ] as $key ) {
+ if ( isset( $params[$key] ) ) {
+ $clientParams[$key] = $params[$key];
+ }
+ }
+ $this->client = new MultiHttpClient( $clientParams );
} else {
$this->client = $params['client'];
}
+ // The parent constructor calls setLogger() which sets the logger in $this->client
+ parent::__construct( $params );
// Make sure URL ends with /
$this->url = rtrim( $params['url'], '/' ) . '/';
// Default config, R+W > N; no locks on reads though; writes go straight to state-machine
$this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_QC;
}
+ public function setLogger( LoggerInterface $logger ) {
+ parent::setLogger( $logger );
+ $this->client->setLogger( $logger );
+ }
+
/**
* @param string $key
* @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
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 ) {
+ // @TODO: respect WRITE_SYNC (e.g. EACH_QUORUM)
+ // @TODO: respect $exptime
$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 ) {
+ if ( $rcode === 200 || $rcode === 201 || $rcode === 204 ) {
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 ) {
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ // @TODO: make this atomic
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime, $flags );
+ }
+
+ return false; // key already set
+ }
+
+ public function delete( $key, $flags = 0 ) {
+ // @TODO: respect WRITE_SYNC (e.g. EACH_QUORUM)
$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 ) {
+ if ( in_array( $rcode, [ 200, 204, 205, 404, 410 ] ) ) {
return true;
}
return $this->handleError( "Failed to delete $key", $rcode, $rerr );
}
+
+ public function incr( $key, $value = 1 ) {
+ // @TODO: make this atomic
+ $n = $this->get( $key, self::READ_LATEST );
+ if ( $this->isInteger( $n ) ) { // key exists?
+ $n = max( $n + intval( $value ), 0 );
+ // @TODO: respect $exptime
+ return $this->set( $key, $n ) ? $n : false;
+ }
+
+ return false;
+ }
}