Implementace Session hadleru

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
pata.kusik111
Člen | 78
+
0
-

Zdravím, snažím se navěsit do legacy Nette Aplikace trochu vylepšený Session handler a zaboha se mi nedaří rozchodit.

Situace:
Máme web s asi 600k session za měsíc (starší jak měsíc cronem mažeme). Do teď jsme je všechny měli s defaultním PHP handlerem v jedné složce, ale to už začíná být problém (protože některé FS mají problém s více jak 1k soubory) v jedné složce. Takže je potřeba změnit systém úložiště.

Jinam než na FS zatím nechceme, byl by to zbytečný overkill a ani si sami nespravujeme server (OPS nám dělá externí kontraktor). Navíc se necheme moc patlat s migracema (jednorázové zahození session je businessově špatné).

Zjistili jsme, že naše session ID je vždy hexadecimální, proto jsme se rozhodli pro 2 úrovně složek, každé úrovneň z prvních 2 písmen ID – 16*16 = 256 složek v každé dalších 256 = 65536 složek, kdyby každá měla cca 1000 souborů, to je 65M session a to nám bohatě stačí.

Používáme ještě Nette 0.9, což je podstatné z těchto důvodů – SafeStream ještě neumí mode „c“ a session_start v Session třídě je obalena custom error handlerem, který i při supresované chybě vyhodí error. Zatím jsem se dostal k tomuto kódu, který ale vyhazuje v tom Session error hadleru Exception bez jakéhokoliv popisku:

<?php
/**
 * Class SessionDirFIleStorage
 *
 * Stores the session in several levels of subdirectories to mitigate the problem of certain filesystems with too many
 * directories/files in one directory level. With 16 characters per position ([0-9a-f]), there will be 16^WIDTH
 * directories per level. The last level will than contain all the remaining sessions starting with the subdirectories
 * name.
 *
 * To smooth out the transition, this class also uses flat directory structure (the default structure of PHP with no
 * subdirectories) as a failover for older sessions. Therefore old (current at the time of introducing this script)
 * sessions are read from the old location, but written into the new subdirectory location.
 */
class SessionDirFileStorage implements \SessionHandlerInterface
{

	const PREFIX = "sess_";
	const DEPTH = 2;
	const WIDTH = 2;

	const OK = 0;
	const ERR_FILE_CREATE = 1;
	const ERR_FILE_LOCK = 2;

	const WAIT_TIME = 20;

	/** @var string */
	private $savePath;
	/** @var resource|false */
	private $filePointer = false;
	/** @var string */
	private $writeFile = null;
	/** @var string */
	private $readFile = null;

	/**
	 * The path from where to read the session data
	 *
	 * Prefer subdirectory, if none, try flat, if none, return subdirectory
	 *
	 * @param $id
	 *
	 * @return string
	 */
	private function getReadPath($id)
	{
		$filename = static::PREFIX . $id;
		$basePath = $this->savePath;
		$subdirs = str_split($id, static::WIDTH);
		for ($depth = 0; $depth < static::DEPTH; $depth = $depth + 1) {
		    $basePath = $basePath . $subdirs[$depth] . DIRECTORY_SEPARATOR;
		}
		if (file_exists($basePath . $filename)) return $basePath;
		if (file_exists($this->savePath . $filename)) return $this->savePath;
		if (!is_dir($basePath)) mkdir($basePath, 0777, true);
		return $basePath;
	}

	/**
	 * The path from where to write the sessiondata
	 *
	 * Always subdirectory structure
	 *
	 * @param $id
	 *
	 * @return string
	 */
	private function getWritePath($id)
	{
		$basePath = $this->savePath;
		$subdirs = str_split($id, static::WIDTH);
		for ($depth = 0; $depth < static::DEPTH; $depth = $depth + 1) {
			$basePath = $basePath . $subdirs[$depth] . DIRECTORY_SEPARATOR;
		}
		if (!is_dir($basePath)) mkdir($basePath, 0777, true);
		return $basePath;
	}

	/**
	 * Called when session_start is called.
	 *
	 * @param string $savePath
	 * @param string $id
	 *
	 * @return bool
	 */
	public function open($savePath, $id)
	{
		if (!in_array('safe', stream_get_wrappers())) {
			SafeStream::register();
		}
		$this->savePath = $savePath;
		if (!is_dir($this->savePath)) {
			mkdir($this->savePath, 0777, true);
		}
		return true;
	}

	private function lock($id)
	{
		$this->readFile = $this->getReadPath($id) . static::PREFIX . $id;
		$this->writeFile = $this->getWritePath($id) . static::PREFIX . $id;
		//This should ensure the file exists
		if (!file_exists($this->writeFile)) {

			/*
			 * Nette bug - The following line would work just fine without Nette.
			 *
			 *  fclose(fopen('safe://' . $this->writeFile, 'a'));
			 *
			 * But Session::start changes the error handler to also listen to suppressed errors (@) and SafeStream
			 * relies on suppressing errors for the mode "a". Therefore, we have to implement the behaviour manually.
			 *
			 * 	$pointer = fopen('safe://' . $this->writeFile, 'x');
			 *	if($pointer !== false) fclose($pointer);
			 *
			 * This could still fail, but not as often.
			 * The final solution is to override the last error thrown...
			 */

			fclose(fopen('safe://' . $this->writeFile, 'a'));
			// I know the previous line will throw an error on not existing file. (SafeStream::107)
			trigger_error(null);
		}
		if (!file_exists($this->writeFile)) return static::ERR_FILE_CREATE;
		//This is where I lock you out
		for ($wait = 1; $wait < static::WAIT_TIME; $wait = $wait * 2) {
			$this->filePointer = fopen('safe://' . $this->writeFile, 'r+');
			if ($this->filePointer !== false) return static::OK;
			if ($wait < static::WAIT_TIME) sleep($wait);
		}
		return static::ERR_FILE_LOCK;
	}

	/**
	 * Reads the data from the session storage
	 *
	 * Read is always called as a part of session_start
	 *
	 * @param string $id
	 *
	 * @return bool|string
	 */
	public function read($id)
	{
		if ($this->filePointer === false || $this->readFile === null || $this->writeFile === null) {
			$lock = $this->lock($id);
			if ($lock !== static::OK) {
				switch ($lock) {
					case static::ERR_FILE_LOCK: {
						trigger_error("Could not secure an exclusive lock on the session file");
						break;
					}
					case static::ERR_FILE_CREATE: {
						trigger_error("Could not create the session file");
						break;
					}
					default: {
						trigger_error("Unknown session locking error");
					}
				}
				return "";
			}
		}
		// read from the locked file, if there was one in the beginning
		if ($this->readFile === $this->writeFile) {
			$filesize = filesize($this->writeFile);
			return $filesize > 0 ? fread($this->filePointer,$filesize) : "";
		}
		// backup file that will be only read, but not written
		if (file_exists($this->readFile)) return (string) file_get_contents($this->readFile);
		return "";
	}

	/**
	 * Close is always called as a part of the script, so it is the ideal place to release the file lock.
	 *
	 * @return bool
	 */
	public function close()
	{
		fclose($this->filePointer);
		return true;
	}

	public function write($id, $data)
	{
		return fwrite($this->filePointer, $data) === false ? false : true;
	}

	public function destroy($id)
	{
		// delete in subdirectory
		if (file_exists($this->writeFile)) unlink('safe://' . $this->writeFile);
		// there won't be any in subdirectory, so it will delete the flat directory
		if (file_exists($this->readFile)) unlink('safe://' . $this->readFile);

		return true;
	}

	/**
	 * Garbage collection is not implemented
	 *
	 * @param int $maxlifetime
	 *
	 * @return bool
	 */
	public function gc($maxlifetime)
	{
		return true;
	}
}

Editoval pata.kusik111 (29. 4. 2016 11:01)

David Matějka
Moderator | 6445
+
0
-

Ahoj, vis, ze tohle chovani podporuje primo PHP upravou hodnoty session.save_path? :) http://php.net/…guration.php#…

pata.kusik111
Člen | 78
+
0
-

David Matějka napsal(a):

Ahoj, vis, ze tohle chovani podporuje primo PHP upravou hodnoty session.save_path? :) http://php.net/…guration.php#…

Ano, toho jsem si vědom, ale to vytvoří složky pouze s jedním písmenem, takže abych dosáhl stejných hodnot, tak bych musel mít 4 úrovně podsložek.

Navíc to neřeší problém se zachováním současných session. Což by se asi dalo vyřešit nějakým skriptem, ale zatímco běží, tak bych musel mít odstávku webu… …a začíná se to zbytečně komplikovat.

Editoval pata.kusik111 (29. 4. 2016 11:04)

fizzy
Backer | 49
+
0
-

tiez sa chystam riesit tento problem, momentalne vsetky session ukladame do temp/session zlozky a momentalne tam je 1 634 836 suborov :D

pata.kusik111
Člen | 78
+
0
-

fizzy napsal(a):

tiez sa chystam riesit tento problem, momentalne vsetky session ukladame do temp/session zlozky a momentalne tam je 1 634 836 suborov :D

Nakonec jsme se stejně rozhodli jít cestou nejmenšího odporu a přesunujeme sessions do REDISu.