Skip to content

Loading…

Added close and forwarding features to native Session #1940

Closed
wants to merge 1 commit into from

4 participants

@dchill42

Simple concurrency solution for native sessions based on discussions in #1746. Allows closing current session with $this->session->native->sess_close() to release file lock and allow session access to concurrent requests. Also offers limited-time forwarding of old sessions to newly regenerated session IDs so concurrent requests don't lose access to current session data when ID is regenerated. Forwarding enabled by $config['sess_forward_window'] = <int>;.

Credit to @GDmac and @Areson for contributions to this solution.

Signed-off-by: dchill42 dchill42@gmail.com

@dchill42

@GDmac and @Areson, here is the new solution we discussed for Native. It needs proper testing - I have only verified that unit tests still pass.

While playing with this implementation, I realized that after session_write_close() read and write access is still available to $_SESSION. The read access may be a nice convenience, but the write access is misleading in that new values are NOT stored to actual session data. I'm thinking we may need to trigger an error on write after close to help devs realize they are out of sync. It's not terribly hard to do, but I'd like your feedback before implementing.

[Edit] Also, do you think there is a need or use for a reopen() function to restore write access after closing? It seemed somewhat pointless to me, but I could see an argument for multiple brief critical sections within a request to facilitate more complex concurrence.

@GDmac

that last bit (reopen) would reserve for a custom library.
you already have to deal with that kind of stuff in the hmvc layer,
for where other controllers might rely on session.

@narfbg

Native sessions implement file locking, which should resolve concurrency problems. Have you ever experienced this problem that you're fixing?

@dchill42

@narfbg Yes, the default file handler for native sessions does use a file lock to prevent concurrent access to the session data. That maintains the integrity of the session data, but it also forces concurrent requests to be sequential. There are scenarios in which this actually creates problems for efficiently handling the requests (especially when using AJAX).

The sess_close() function added to this driver (but not the Session library at large) gives the developer the opportunity to make changes to session data and then release that file lock so other requests can access the session while the first request continues to do other processing. This supports better overlapping of concurrent requests, especially when a long request overlaps with a very short one (or more). Usage is completely optional.

The forwarding feature is enabled by setting the new configuration variable, also making it completely optional. When set to a positive number of seconds, it engages a mechanism that allows concurrent requests to "catch up" with a regenerated session ID. If the client sends a request which will trigger an ID regen, and another request is initiated before the new ID is returned to the client, the second one will try to access an orphaned session with the old ID, ruining the session state maintained in the data. This feature allows the old session to be "forwarded" to the new session for a brief (developer specified) window in order to alleviate that problem.

These are issues that have been tested and verified, and there is a certain amount of demand for a solution. You have already seen a handful of requests related to these problems, such as #154, #1283, #1713, #1900, and probably others. There is also a very well-written article which explains the trouble in detail and compares a variety of solutions.

@dchill42 dchill42 Added close and forwarding features to native Session
Signed-off-by: dchill42 <dchill42@gmail.com>
733b824
@Areson

I've run my battery of tests for session forwarding and it looks like we need a few tweaks to get it operating as expected. I'll make a couple of comments on the commit on the specifics I noticed.

[Edit]
In general:

  • We need to ge careful where and when calling sess_destroy() and session_destroy() during session forwarding.
  • I don't think setting last_activity on every read is the right behavior, as frequent requests prevent the session from regenerating. We should probably only set it for newly created sessions and when the session is regenerated.
  • We need to make sure that an old session id that has already regenerated is no longer allowed to call regenerate.
  • Setting the $_COOKIE value is not enough during forwarding. We need to call session_id($new_id)

[Another Edit]
Just a quick apology for all of the comment spam. All in all it looks pretty good, and it's a nice and simple implementation. Great job so far @dchill42.

@Areson

Don't call sess_destroy() here. We lose our now correct cookie and our $_SESSION data for future requests coming in with the old session id that still need forwarding. We can call a session_write_close in the if statement below.

In this condition, the current session has the old session ID and the session data contains only the forwarding data. We want that cookie to be replaced with the new value. Did you encounter a problem with the forwarding during testing? Perhaps I overlooked something, but I believe the general logic here is correct.

I did run into a problem during forwarding. If we have multiple requests that need to be forwarded, they all rely on having access to the session data for the old session id. If we destroy it, the forwarding will fail for all but the first request that we forward. That's why I suggested removing this delete. This does mean that we will have to rely on the built-in GC for native sessions to clean up the old data.

Oh! It's wiping out the forwarding data, isn't it? Oops - that was not the intended effect, I'll fix that.

@Areson

Add a call to session_id($new_id) here to make sure we really start the new session. This will cause a new cookie to drop, but that is fine as long as our sess_time_to_update window is larger than our sess_forward_window. Also, if we have any flags/values used with the old session id, we will need to unset them here before starting the new session, or they get carried over.

The session_id() call is probably a better solution than setting $_COOKIE directly. I'll change that.

@Areson

We can add an else here to destroy the session safely if we get a request after the window has close. Note that calling sess_destroy rather than session_destroy() will cause any cookie with a valid session id to go away, which means dropped sessions. This may be the behavior we want.

@Areson

We want to protect this section from being called by an old session id once the session has been regenerated. We can check for the presence of $_SESSION['sess_new_id'] and prevent code from running if it is set as long as we unset it above when forwarding takes place (otherwise it remains set on the new session id).

@Areson

We should update the $_SESSION['last_activity'] value here.

@narfbg

Superseded by #3073.

@narfbg narfbg closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 29, 2012
  1. @dchill42

    Added close and forwarding features to native Session

    dchill42 committed
    Signed-off-by: dchill42 <dchill42@gmail.com>
This page is out of date. Refresh to see the latest.
Showing with 85 additions and 8 deletions.
  1. +3 −1 application/config/config.php
  2. +82 −7 system/libraries/Session/drivers/Session_native.php
View
4 application/config/config.php
@@ -279,6 +279,7 @@
| 'sess_match_ip' = Whether to match the user's IP address when reading the session data
| 'sess_match_useragent' = Whether to match the User Agent when reading the session data
| 'sess_time_to_update' = how many seconds between CI refreshing Session Information
+| 'sess_forward_window' = how long to forward sessions to regenerated ID in seconds
|
*/
$config['sess_driver'] = 'cookie';
@@ -292,6 +293,7 @@
$config['sess_match_ip'] = FALSE;
$config['sess_match_useragent'] = TRUE;
$config['sess_time_to_update'] = 300;
+$config['sess_forward_window'] = FALSE;
/*
|--------------------------------------------------------------------------
@@ -421,4 +423,4 @@
/* End of file config.php */
-/* Location: ./application/config/config.php */
+/* Location: ./application/config/config.php */
View
89 system/libraries/Session/drivers/Session_native.php
@@ -37,6 +37,8 @@
*/
class CI_Session_native extends CI_Session_driver {
+ protected $forwarding = FALSE;
+
/**
* Initialize session driver object
*
@@ -53,6 +55,7 @@ protected function initialize()
'sess_match_ip',
'sess_match_useragent',
'sess_time_to_update',
+ 'sess_forward_window',
'cookie_prefix',
'cookie_path',
'cookie_domain',
@@ -68,16 +71,17 @@ protected function initialize()
}
// Set session name, if specified
+ $sess_name = '';
if ($config['sess_cookie_name'])
{
// Differentiate name from cookie driver with '_id' suffix
- $name = $config['sess_cookie_name'].'_id';
+ $sess_name = $config['sess_cookie_name'].'_id';
if ($config['cookie_prefix'])
{
// Prepend cookie prefix
- $name = $config['cookie_prefix'].$name;
+ $sess_name = $config['cookie_prefix'].$sess_name;
}
- session_name($name);
+ session_name($sess_name);
}
// Set expiration, path, and domain
@@ -105,13 +109,39 @@ protected function initialize()
$domain = $config['cookie_domain'];
}
+ if ($config['sess_forward_window'] && $config['sess_forward_window'] > 0)
+ {
+ // Save forwarding window
+ $this->forwarding = $config['sess_forward_window'];
+ }
+
session_set_cookie_params($config['sess_expire_on_close'] ? 0 : $expire, $path, $domain, $secure, $http_only);
// Start session
session_start();
- // Check session expiration, ip, and agent
+ // Check for session forwarding
$now = time();
+ if ($this->forwarding && isset($_SESSION['sess_new_id']))
+ {
+ // Get new ID and fowarding expiration and destroy old session
+ $new_id = $_SESSION['sess_new_id'];
+ $expires = isset($_SESSION['fwd_expires']) ? $_SESSION['fwd_expires'] : 0;
+ $this->sess_destroy();
+
+ // Check expiration
+ if ($now < $expires)
+ {
+ // Forward to new session
+ $name = $sess_name ? $sess_name : session_name();
+ $_COOKIE[$sess_name] = $new_id;
+ }
+
+ // Start new session
+ session_start();
+ }
+
+ // Check session expiration, ip, and agent
$destroy = FALSE;
if (isset($_SESSION['last_activity']) && (($_SESSION['last_activity'] + $expire) < $now OR $_SESSION['last_activity'] > $now))
{
@@ -144,7 +174,7 @@ protected function initialize()
&& ($_SESSION['last_activity'] + $config['sess_time_to_update']) < $now)
{
// Changing the session ID amidst a series of AJAX calls causes problems
- if( ! $this->CI->input->is_ajax_request())
+ if($this->forwarding OR ! $this->CI->input->is_ajax_request())
{
// Regenerate ID, but don't destroy session
$this->sess_regenerate(FALSE);
@@ -186,6 +216,19 @@ public function sess_save()
// ------------------------------------------------------------------------
/**
+ * Close session and release locks
+ *
+ * @return void
+ */
+ public function sess_close()
+ {
+ // Close session - releases file lock
+ session_write_close();
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
* Destroy the current session
*
* @return void
@@ -217,8 +260,40 @@ public function sess_destroy()
*/
public function sess_regenerate($destroy = FALSE)
{
- // Just regenerate id, passing destroy flag
- session_regenerate_id($destroy);
+ // Check for session forwarding
+ if ($this->forwarding)
+ {
+ // Generate new session ID
+ // We use the same method as php_session_create_id - the default
+ // generator in the PHP session extension
+ $addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
+ $time = gettimeofday();
+ $id = md5(sprintf('%.15s%ld%ld%0.8F', $addr, $time['sec'], $time['usec'], lcg_value()));
+
+ // Replace current session data
+ if ( ! $destroy)
+ {
+ $data = $_SESSION;
+ }
+ $_SESSION = array('sess_new_id' => $id, 'fwd_expires' => time() + $this->forwarding);
+
+ // Close session and open new
+ session_write_close();
+ session_id($id);
+ session_start();
+
+ // Restore session data
+ if ( ! $destroy)
+ {
+ $_SESSION = $data;
+ }
+ }
+ else
+ {
+ // Just regenerate id, passing destroy flag
+ session_regenerate_id($destroy);
+ }
+
$_SESSION['session_id'] = session_id();
}
Something went wrong with that request. Please try again.