Skip to content

Commit 941d6e1

Browse files
CLIENT-3777 Fix anomalous getTtl() behavior
This patch greatly simplifies the getTtl() logic in the original PHP Rust bindings. It turns out that the Aerospike server itself maintains the TTL on behalf of the client; there is no need for the PHP client to maintain CITRUSLEAF_EPOCH-relative timestamps. This is in line with both the C and Java clients, thus implying consistent behavior with Javascript and Python clients as well. The Expiration interface has been upgraded as well; it is fully backward compatible with previous code, but now has getters which allows software to query its current state. In addition to this change, a number of PHPUnit tests have been added which exercises the new behavior along with the Expiration type's new interface.
1 parent 0bf4e2b commit 941d6e1

File tree

4 files changed

+136
-138
lines changed

4 files changed

+136
-138
lines changed

DEBUG-sfalvo/put.php

Lines changed: 0 additions & 97 deletions
This file was deleted.

php_stubs/libaerospike-php-stubsv1.0.0.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,26 +3205,58 @@ public static function getByValueRelativeRankRangeCount(string $bin_name, mixed
32053205
*/
32063206
class Expiration {
32073207
/**
3208-
* Set the record to expire X seconds from now
3208+
* Set the record to expire X seconds from now. See also `getTtl()`.
32093209
*/
32103210
public static function Seconds(int $seconds): \Aerospike\Expiration {}
32113211

32123212
/**
3213-
* Set the record's expiry time using the default time-to-live (TTL) value for the namespace
3213+
* Answers with the expiration's current time to live in units of
3214+
* seconds, excluding any special values. If the expiration is set to
3215+
* the namespace default, is configured to never update, or is configured
3216+
* to never expire, this method returns null. See also `Seconds()`.
3217+
*/
3218+
public function get_ttl(): ?int {}
3219+
3220+
/**
3221+
* Set the record's expiry time using the default time-to-live (TTL) value
3222+
* for the namespace. See also `isNamespaceDefault()`.
32143223
*/
32153224
public static function NamespaceDefault(): \Aerospike\Expiration {}
32163225

3226+
/**
3227+
* Answers true only if the expiration is set to use the namespace default.
3228+
* See also `NamespaceDefault()`.
3229+
*/
3230+
public function isNamespaceDefault(): bool {}
3231+
32173232
/**
32183233
* Set the record to never expire. Requires Aerospike 2 server version 2.7.2 or later or
32193234
* Aerospike 3 server version 3.1.4 or later. Do not use with older servers.
3235+
* See also `willNeverExpire()`.
32203236
*/
32213237
public static function Never(): \Aerospike\Expiration {}
32223238

32233239
/**
3224-
* Do not change the record's expiry time when updating the record; requires Aerospike server
3225-
* version 3.10.1 or later.
3240+
* Answers true only if the expiration is set to never expire.
3241+
* See also `Never()`.
3242+
*/
3243+
public function willNeverExpire(): bool {}
3244+
3245+
/**
3246+
* Do not change the record's expiry time when updating the record;
3247+
* requires Aerospike server version 3.10.1 or later.
3248+
* See also `willUpdateExpiration()`.
32263249
*/
32273250
public static function DontUpdate(): \Aerospike\Expiration {}
3251+
3252+
/**
3253+
* Answers *true* if the expiration is configured to somehow change during
3254+
* a record update. This can be as simple as an explicit time-to-live, or
3255+
* an instruction to use the namespace's default expiration, etc. Answers
3256+
* *false* if the expiration will *not* be changed during a record update
3257+
* (e.g., the expiration was constructed with DontUpdate().)
3258+
*/
3259+
public function willUpdateExpiration(): bool {}
32283260
}
32293261

32303262
/**

src/lib.rs

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use std::hash::{Hash, Hasher};
2727
use std::io::Cursor;
2828
use std::sync::Arc;
2929
use std::sync::Mutex;
30-
use std::time::SystemTime;
3130

3231
use byteorder::{ByteOrder, NetworkEndian};
3332
use ripemd160::digest::Digest;
@@ -67,7 +66,6 @@ pub type AsResult<T = ()> = std::result::Result<T, AerospikeException>;
6766

6867
const VERSION: &str = env!("CARGO_PKG_VERSION");
6968
const PARTITIONS: u16 = 4096;
70-
const CITRUSLEAF_EPOCH: u64 = 1262304000;
7169

7270
////////////////////////////////////////////////////////////////////////////////////////////
7371
//
@@ -1602,7 +1600,7 @@ const NEVER_EXPIRE: u32 = 0xFFFF_FFFF; // -1 as i32
16021600
const DONT_UPDATE: u32 = 0xFFFF_FFFE;
16031601

16041602
/// Record expiration, also known as time-to-live (TTL).
1605-
#[derive(Debug, Clone, Copy)]
1603+
#[derive(Debug, Clone, Copy, PartialEq)]
16061604
pub enum _Expiration {
16071605
Seconds(u32),
16081606
NamespaceDefault,
@@ -1628,41 +1626,73 @@ impl FromZval<'_> for Expiration {
16281626
#[php_impl]
16291627
#[derive(ZvalConvert)]
16301628
impl Expiration {
1631-
/// Set the record to expire X seconds from now
1629+
/// Set the record to expire X seconds from now. See also `getTtl()`.
16321630
pub fn Seconds(seconds: u32) -> Self {
16331631
Expiration {
16341632
_as: _Expiration::Seconds(seconds),
16351633
}
16361634
}
16371635

1638-
/// Set the record's expiry time using the default time-to-live (TTL) value for the namespace
1636+
/// Answers with the expiration's current time to live in units of
1637+
/// seconds, excluding any special values. If the expiration is set to
1638+
/// the namespace default, is configured to never update, or is configured
1639+
/// to never expire, this method returns null. See also `Seconds()`.
1640+
#[getter]
1641+
pub fn get_ttl(&self) -> Option<u32> {
1642+
match self._as {
1643+
_Expiration::Seconds(secs) => Some(secs),
1644+
_ => None,
1645+
}
1646+
}
1647+
1648+
/// Set the record's expiry time using the default time-to-live (TTL) value
1649+
/// for the namespace. See also `isNamespaceDefault()`.
16391650
pub fn Namespace_Default() -> Self {
16401651
Expiration {
16411652
_as: _Expiration::NamespaceDefault,
16421653
}
16431654
}
16441655

1656+
/// Answers true only if the expiration is set to use the namespace default.
1657+
/// See also `NamespaceDefault()`.
1658+
#[getter]
1659+
pub fn is_namespace_default(&self) -> bool {
1660+
self._as == _Expiration::NamespaceDefault
1661+
}
1662+
16451663
/// Set the record to never expire. Requires Aerospike 2 server version 2.7.2 or later or
16461664
/// Aerospike 3 server version 3.1.4 or later. Do not use with older servers.
1665+
/// See also `willNeverExpire()`.
16471666
pub fn Never() -> Self {
16481667
Expiration {
16491668
_as: _Expiration::Never,
16501669
}
16511670
}
16521671

1653-
/// Do not change the record's expiry time when updating the record; requires Aerospike server
1654-
/// version 3.10.1 or later.
1672+
/// Answers true only if the expiration is set to never expire.
1673+
/// See also `Never()`.
1674+
pub fn will_never_expire(&self) -> bool {
1675+
self._as == _Expiration::Never
1676+
}
1677+
1678+
/// Do not change the record's expiry time when updating the record;
1679+
/// requires Aerospike server version 3.10.1 or later.
1680+
/// See also `willUpdateExpiration()`.
16551681
pub fn Dont_Update() -> Self {
16561682
Expiration {
16571683
_as: _Expiration::DontUpdate,
16581684
}
16591685
}
16601686

1661-
/// Answers with the expiration's configured value in units of seconds.
1662-
#[getter]
1663-
pub fn in_seconds(&self) -> u32 {
1664-
self.into()
1665-
}
1687+
/// Answers *true* if the expiration is configured to somehow change during
1688+
/// a record update. This can be as simple as an explicit time-to-live, or
1689+
/// an instruction to use the namespace's default expiration, etc. Answers
1690+
/// *false* if the expiration will *not* be changed during a record update
1691+
/// (e.g., the expiration was constructed with DontUpdate().)
1692+
#[getter]
1693+
pub fn will_update_expiration(&self) -> bool {
1694+
self._as != _Expiration::DontUpdate
1695+
}
16661696
}
16671697

16681698
impl From<&Expiration> for u32 {
@@ -4217,8 +4247,9 @@ impl Record {
42174247
Some(self._as.generation)
42184248
}
42194249

4220-
/// Expiration is TTL (Time-To-Live).
4221-
/// Number of seconds until record expires.
4250+
/// Expiration indicates when a record will expire (Time-To-Live).
4251+
/// To determine a record's time to live, use the `getTtl()` method on the
4252+
/// Expiration or, equivalently, on the record.
42224253
#[getter]
42234254
pub fn get_expiration(&self) -> Expiration {
42244255
match self._as.expiration {
@@ -4227,28 +4258,12 @@ impl Record {
42274258
}
42284259
}
42294260

4230-
/// Expiration is TTL (Time-To-Live).
4231-
/// Number of seconds until record expires.
4261+
/// Answer with the record's TTL (Time-To-Live), or null if not
4262+
/// possible. Expressed in number of seconds until record expires.
4263+
/// Equivalent to `$this->getExpiration()->getTtl()`.
42324264
#[getter]
42334265
pub fn get_ttl(&self) -> Option<u32> {
4234-
match self._as.expiration {
4235-
0 => NEVER_EXPIRE.into(),
4236-
secs => {
4237-
let expiration = CITRUSLEAF_EPOCH + (secs as u64);
4238-
let now = SystemTime::now()
4239-
.duration_since(SystemTime::UNIX_EPOCH)
4240-
.unwrap()
4241-
.as_secs();
4242-
4243-
// Record may not have expired on server, but delay or clock differences may
4244-
// cause it to look expired on client. Floor at 1, not 0, to avoid old
4245-
// "never expires" interpretation.
4246-
if expiration > now {
4247-
return (((expiration as u64) - now) as u32).into();
4248-
}
4249-
return (1 as u32).into();
4250-
}
4251-
}
4266+
self.get_expiration().get_ttl()
42524267
}
42534268

42544269
/// Key is the record's key.
@@ -9750,7 +9765,6 @@ impl Client {
97509765
/// Write record bin(s). The policy specifies the transaction timeout, record expiration and
97519766
/// how the transaction is handled when the record already exists.
97529767
pub fn put(&self, policy: &WritePolicy, key: &Key, bins: Vec<&Bin>) -> PhpResult<()> {
9753-
eprintln!("LBPUT001 put() entered");
97549768
let bins: Vec<proto::Bin> = bins.into_iter().map(|b| b.into()).collect();
97559769

97569770
let request = tonic::Request::new(proto::AerospikePutRequest {
@@ -9768,7 +9782,6 @@ impl Client {
97689782
} => Ok(()),
97699783
pe => {
97709784
let error: AerospikeException = pe.into();
9771-
eprintln!("LBPUT002 Error detected: {:?}", error);
97729785
throw_object(error.into_zval(true)?)?;
97739786
Ok(())
97749787
}

tests/ClientTest.php

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public static function setUpBeforeClass(): void
2424
}
2525
}
2626

27-
2827
public function testPutGetValues()
2928
{
3029
$values = [
@@ -319,4 +318,55 @@ public function testPutGetBinary()
319318

320319
$this->assertEquals($binary, $binGet["binaryBin"]);
321320
}
321+
322+
public function testReadTtlExpires()
323+
{
324+
$stringKey = new Key(self::$namespace, self::$set, "new_key");
325+
$wp = new WritePolicy();
326+
$wp->setExpiration(Expiration::Seconds(3));
327+
self::$client->put($wp, $stringKey, [new Bin("record", "expires_in_3")]);
328+
sleep(5);
329+
$rp = new ReadPolicy();
330+
$record = self::$client->get($rp, $stringKey);
331+
$this->assertEquals($record, null);
332+
}
333+
334+
public function testReadTtlNoExpires()
335+
{
336+
$stringKey = new Key(self::$namespace, self::$set, "new_key");
337+
$wp = new WritePolicy();
338+
$wp->setExpiration(Expiration::Seconds(3));
339+
self::$client->put($wp, $stringKey, [new Bin("record", "expires_in_3")]);
340+
sleep(1);
341+
$rp = new ReadPolicy();
342+
$record = self::$client->get($rp, $stringKey);
343+
$this->assertEquals($record->getTtl(), 2);
344+
}
345+
346+
public function testReadTtlNotUpdated()
347+
{
348+
$stringKey = new Key(self::$namespace, self::$set, "new_key");
349+
$wp = new WritePolicy();
350+
$wp->setExpiration(Expiration::Seconds(3));
351+
self::$client->put($wp, $stringKey, [new Bin("record", "expires_in_3")]);
352+
sleep(1);
353+
$wp->setExpiration(Expiration::DontUpdate());
354+
$this->assertFalse($wp->getExpiration()->willUpdateExpiration());
355+
self::$client->put($wp, $stringKey, [new Bin("record", "expires_in_2_hopefully")]);
356+
$rp = new ReadPolicy();
357+
$record = self::$client->get($rp, $stringKey);
358+
$this->assertEquals($record->getTtl(), 2);
359+
}
360+
361+
public function testReadTtlNeverExpires()
362+
{
363+
$stringKey = new Key(self::$namespace, self::$set, "new_key");
364+
$wp = new WritePolicy();
365+
$wp->setExpiration(Expiration::Never());
366+
self::$client->put($wp, $stringKey, [new Bin("record", "records_are_forever")]);
367+
$rp = new ReadPolicy();
368+
$record = self::$client->get($rp, $stringKey);
369+
$this->assertEquals($record->getTtl(), null);
370+
$this->assertTrue($record->getExpiration()->willNeverExpire());
371+
}
322372
}

0 commit comments

Comments
 (0)