Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[HttpFoundation] MongoDbSessionHandler supports auto expiry via configurable expiry_field #11510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ public function __construct($mongo, array $options)
$this->mongo = $mongo;

$this->options = array_merge(array(
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'expiry_field' => false,
), $options);
}

Expand Down Expand Up @@ -109,6 +110,9 @@ public function gc($maxlifetime)
*
* See: http://docs.mongodb.org/manual/tutorial/expire-data/
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than most of the existing method body in an if statement, it would be preferable to simply do the following at the top of the function:

if (false !== $this->options['expiry_field']) {
    return true;
}

That keeps the diff smaller, which benefits anyone working on the file in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (false !== $this->options['expiry_field']) {
return true;
}
$time = new \MongoDate(time() - $maxlifetime);

$this->getCollection()->remove(array(
Expand All @@ -123,12 +127,27 @@ public function gc($maxlifetime)
*/
public function write($sessionId, $data)
{
$fields = array(
$this->options['data_field'] => new \MongoBinData($data, \MongoBinData::BYTE_ARRAY),
$this->options['time_field'] => new \MongoDate(),
);

/* Note: As discussed in the gc method of this class. You can utilise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment should be in the phpdoc where it is actually visible to users

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best to submit a new PR with that chance, since this has already been merged and closed.

* TTL collections in MongoDB 2.2+
* We are setting the "expiry_field" as part of the write operation here
* You will need to create the index on your collection that expires documents
* at that time
* e.g.
* db.MySessionCollection.ensureIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )
*/
if (false !== $this->options['expiry_field']) {
$expiry = new \MongoDate(time() + (int) ini_get('session.gc_maxlifetime'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tobion: In #11508 (comment), you mentioned that the session lifetime option in Symfony sets the session.gc_maxlifetime INI value. Do you have a reference for this? I couldn't find it.

One concern I have is that we're storing the option's value from a particular point in time in this calculated value. If the PHP configuration changes because session lifetimes need to be extended site-wide, this storage layer is still going to delete those records based on the original time. I would assume other storage layers would simply delay their gc() step and allow session data to persist. That seems like a caveat of using TTL indexes with expireAfterSeconds of 0 and not relying on PHP for garbage collection.

Just for context, ZF's MongoDB session handler stores the lifetime (i.e. session.gc_maxlifetime) with each write. That implementation requires that the session lifetime is kept in sync with the TTL index's expireAfterSeconds option, but it ensures that behavior between MongoDB's deletion routine and PHP's garbage collection are consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the PHP configuration changes because session lifetimes need to be extended site-wide, this storage layer is still going to delete those records based on the original time.

But only for records that have not had activity, as they will have the new expireAt set based on whatever the maxlifetime may have been changed to.

Not sure how much of an issue this actually is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But only for records that have not had activity, as they will have the new expireAt set based on whatever the maxlifetime may have been changed to.

Good point! I missed that in my initial comment. No objection then, provided Symfony is OK with reading the session.gc_maxlifetime INI value here (I'm still curious what @Tobion was referring to).

$fields[$this->options['expiry_field']] = $expiry;
}

$this->getCollection()->update(
array($this->options['id_field'] => $sessionId),
array('$set' => array(
$this->options['data_field'] => new \MongoBinData($data, \MongoBinData::BYTE_ARRAY),
$this->options['time_field'] => new \MongoDate(),
)),
array('$set' => $fields),
array('upsert' => true, 'multiple' => false)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ protected function setUp()
'data_field' => 'data',
'time_field' => 'time',
'database' => 'sf2-test',
'collection' => 'session-test'
'collection' => 'session-test',
);

$this->storage = new MongoDbSessionHandler($this->mongo, $this->options);
Expand Down Expand Up @@ -100,6 +100,45 @@ public function testWrite()
$that->assertInstanceOf('MongoDate', $data[$this->options['time_field']]);
}

public function testWriteWhenUsingExpiresField()
{
$this->options = array(
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'database' => 'sf2-test',
'collection' => 'session-test',
'expiry_field' => 'expiresAt'
);

$this->storage = new MongoDbSessionHandler($this->mongo, $this->options);

$collection = $this->createMongoCollectionMock();

$this->mongo->expects($this->once())
->method('selectCollection')
->with($this->options['database'], $this->options['collection'])
->will($this->returnValue($collection));

$that = $this;
$data = array();

$collection->expects($this->once())
->method('update')
->will($this->returnCallback(function ($criteria, $updateData, $options) use ($that, &$data) {
$that->assertEquals(array($that->options['id_field'] => 'foo'), $criteria);
$that->assertEquals(array('upsert' => true, 'multiple' => false), $options);

$data = $updateData['$set'];
}));

$this->assertTrue($this->storage->write('foo', 'bar'));

$this->assertEquals('bar', $data[$this->options['data_field']]->bin);
$that->assertInstanceOf('MongoDate', $data[$this->options['time_field']]);
$that->assertInstanceOf('MongoDate', $data[$this->options['expiry_field']]);
}

public function testReplaceSessionData()
{
$collection = $this->createMongoCollectionMock();
Expand Down Expand Up @@ -154,10 +193,36 @@ public function testGc()
->method('remove')
->will($this->returnCallback(function ($criteria) use ($that) {
$that->assertInstanceOf('MongoDate', $criteria[$that->options['time_field']]['$lt']);
$that->assertGreaterThanOrEqual(time() - -1, $criteria[$that->options['time_field']]['$lt']->sec);
$that->assertGreaterThanOrEqual(time() - 1, $criteria[$that->options['time_field']]['$lt']->sec);
}));

$this->assertTrue($this->storage->gc(-1));
$this->assertTrue($this->storage->gc(1));
}

public function testGcWhenUsingExpiresField()
{
$this->options = array(
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'database' => 'sf2-test',
'collection' => 'session-test',
'expiry_field' => 'expiresAt'
);

$this->storage = new MongoDbSessionHandler($this->mongo, $this->options);

$collection = $this->createMongoCollectionMock();

$this->mongo->expects($this->never())
->method('selectCollection');

$that = $this;

$collection->expects($this->never())
->method('remove');

$this->assertTrue($this->storage->gc(1));
}

private function createMongoCollectionMock()
Expand Down