• Revista PROGRAMAR: Já está disponível a edição #53 da revista programar. Faz já o download aqui!

vitortomaz

[PHP] Incremento atómico de numero em ficheiro (para estatísticas da revista PaP)

28 mensagens neste tópico

boas,

esta foi a minha primeira incursão em PHP visto que "sou" de ASP.NET

tentei criar uma função que faça os incrementos sem problemas de concorrência fazendo lock ao ficheiro...

não sei se a sintaxe está toda bem, o que é que o pessoal de PHP diz a isto?

<?php
class PaPsafeIncrementer
{
public static function safeIncrement($arquivo)
{
	$mode = 'r+';
	$fp = fopen($arquivo, $mode);
	$retries = 0;
	$max_retries = 100;

	if (!$fp) {
		// failure
		return false;
	}

	// keep trying to get a lock as long as possible
	do {
		if ($retries > 0) {
			usleep(rand(1, 10000));
		}
		$retries += 1;
	} while (!flock($fp, LOCK_EX) and $retries <= $max_retries);

	// couldn't get the lock, give up
	if ($retries == $max_retries) {
		// failure
		return false;
	}

	//read the data from the file and increment
	$data=file_get_contents($arquivo)+1;

	// got the lock, write the data
	fwrite($fp, $data);
	// release the lock
	flock($fp, LOCK_UN);
	fclose($fp);
	// success
	return true;
}
}
?>

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Quem souber PHP que dê umas dicas, pois temos tido alguns problemas com o método das estatísticas que se tem usado até agora...

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

$counter_file_name = "counter_ed15_site.txt";

while (file_exists($counter_file_name.".tmp")){    
    usleep(100000);
}
$tmp_file = fopen($counter_file_name.".tmp", "w");
fclose($tmp_file);
file_put_contents($counter_file_name, file_get_contents($counter_file_name)+1);
unlink($counter_file_name.".tmp");

Enquanto o ficheiro está a ser editado é criado um ficheiro temporário, enquanto esse ficheiro existir o outro está protegido.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Enquanto o ficheiro está a ser editado é criado um ficheiro temporário, enquanto esse ficheiro existir o outro está protegido.

entre o teste se o ficheiro existe e a criação do temporário existe uma "janela de oportunidade" não?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Não é preciso uma class para isso pá ;)

pois, secalhar não  :-[, como digo, foi a minha primeira incursão em php  ;)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

entre o teste se o ficheiro existe e a criação do temporário existe uma "janela de oportunidade" não?

Hever há... (mas eu acho que tenho solução)
0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Bom... aqui vai.

Bom trabalho vitor ;) Mas confesso que não gosto muito do modelo. À partida parece estar tudo bem, mas em produção poderá criar problemas. O flock tem problemas, e tem dado mais dores de cabeça do que soluções. Tem a capacidade de deixar de funcionar sem nenhuma razão aparente e deitar todo um website abaixo por causa de um dead-lock.

Actualmente a realidade é esta: ficheiros flat (flat file) em PHP não são um método seguro para armazenar informação se existe uma necessidade de concurrent access. flock() é uma resposta, mas ainda precisa de trabalho (muito).

Que solução então? Bom depende de várias coisas. À partida o que eu proponho é fazer o counter com uma base de dados. Se não tiverem acesso a um RDBMS (muitos provedores não facilitam quando se trata dos seus pacotes mais baratos) usem o SQLite que não precisa de nada instalado a nível de servidor. Uma BD é a melhor solução porque resolve de uma vez por todas o problema de acessos concorrentes.

Outra solução (se realmente tem de ser feito com ficheiros) é utilizar um método de check circular com back-reference. Isto ajudará: http://pt2.php.net/manual/en/function.fopen.php#78370

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

usando a sugestão do Marfig o que poderia ser feito era isto certo?  :hmm:

<?php
$dir_fileopen = "../AN/INTERNAL/DIRECTORY/fileopen";

function randomid() {
return time().substr(md5(microtime()), 0, rand(5, 12));
}

function cfopen($filename, $mode, $overwriteanyway = false) {
global $dir_fileopen;
clearstatcache();
do {
	$id = md5(randomid(rand(), TRUE));
	$tempfilename = $dir_fileopen."/".$id.md5($filename);
} while(file_exists($tempfilename));
if (file_exists($filename)) {
	$newfile = false;
	copy($filename, $tempfilename);
}else{
	$newfile = true;
}
$fp = fopen($tempfilename, $mode);
return $fp ? array($fp, $filename, $id, @filemtime($filename), $newfile, $overwriteanyway) : false;
}

function cfwrite($fp,$string) { return fwrite($fp[0], $string); }

function cfclose($fp, $debug = "off") {
global $dir_fileopen;
$success = fclose($fp[0]);
clearstatcache();
$tempfilename = $dir_fileopen."/".$fp[2].md5($fp[1]);
if ((@filemtime($fp[1]) == $fp[3]) or ($fp[4]==true and !file_exists($fp[1])) or $fp[5]==true) {
	rename($tempfilename, $fp[1]);
}else{
	unlink($tempfilename);
if ($debug != "off") echo "While writing, another process accessed $fp[1]. To ensure file-integrity, your changes were rejected.";
$success = false;
}
return $success;
}


//file_put_contents("counter_ed15_site.txt", file_get_contents("counter_ed15_site.txt")+1);
$filename="counter_ed15_site.txt"
$mode="r+";
$max_retries =10;
$retries=0;

do {	
$retries += 1;
$fp = cfopen($filename, $mode);
cfwrite($fp, file_get_contents($filename)+1);
} while (!cfclose($fp) and $retries <= $max_retries);

?>

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Olhando apenas por alto parece-me bem :)

Eu sei que é algo estranho tanto código para fazer um counter. Mas acredito que estás mais à procura de aprender o PHP, pelo que este tipo de problemas (e as soluções que se lhes apresentam) são a melhor forma de ganhares um conhecmento sólido da linguagem e também das suas limitações.

Num caso real, nunca irias querer fazer isto. Mais uma vez, utilizarias uma base de dados e voilá! ;)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Olhando apenas por alto parece-me bem :P

Eu sei que é algo estranho tanto código para fazer um counter. Mas acredito que estás mais à procura de aprender o PHP, pelo que este tipo de problemas (e as soluções que se lhes apresentam) são a melhor forma de ganhares um conhecmento sólido da linguagem e também das suas limitações.

Num caso real, nunca irias querer fazer isto. Mais uma vez, utilizarias uma base de dados e voilá! ;)

É para um caso real. Está-se a evitar o uso de um SGBD :)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Versão simplificada

function safeIncrement($arquivo) {
$retries = 0;
$max_retries = 100;


if (!($fp = fopen($arquivo, 'r+'))) return false; // fail

// keep trying to get a lock as long as possible
while (!flock($fp, LOCK_EX) {
	usleep(100);
	if(++$retries > $max_retries) return false; // fail
}



$data = file_get_contents($arquivo)++;
// got the lock, write the data
fwrite($fp, $data);
// release the lock
flock($fp, LOCK_UN);
fclose($fp);


return $data;
}

Bom... aqui vai.

Bom trabalho vitor :) Mas confesso que não gosto muito do modelo. À partida parece estar tudo bem, mas em produção poderá criar problemas. O flock tem problemas, e tem dado mais dores de cabeça do que soluções. Tem a capacidade de deixar de funcionar sem nenhuma razão aparente e deitar todo um website abaixo por causa de um dead-lock.

Actualmente a realidade é esta: ficheiros flat (flat file) em PHP não são um método seguro para armazenar informação se existe uma necessidade de concurrent access. flock() é uma resposta, mas ainda precisa de trabalho (muito).

Que solução então? Bom depende de várias coisas. À partida o que eu proponho é fazer o counter com uma base de dados. Se não tiverem acesso a um RDBMS (muitos provedores não facilitam quando se trata dos seus pacotes mais baratos) usem o SQLite que não precisa de nada instalado a nível de servidor. Uma BD é a melhor solução porque resolve de uma vez por todas o problema de acessos concorrentes.

Outra solução (se realmente tem de ser feito com ficheiros) é utilizar um método de check circular com back-reference. Isto ajudará: http://pt2.php.net/manual/en/function.fopen.php#78370

Nem mais. Dependendo daquilo para que precisamos do lock/wtv, até podemos ter soluções mais eficientes que as que apresentaste.

É para um caso real. Está-se a evitar o uso de um SGBD ;)

Isso não quer dizer que não se vá usar no futuro.
0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Versão simplificada

function safeIncrement($arquivo) {
$retries = 0;
$max_retries = 100;


if (!($fp = fopen($arquivo, 'r+'))) return false; // fail

// keep trying to get a lock as long as possible
while (!flock($fp, LOCK_EX) {
	usleep(100);
	if(++$retries > $max_retries) return false; // fail
}



$data = file_get_contents($arquivo)++;
// got the lock, write the data
fwrite($fp, $data);
// release the lock
flock($fp, LOCK_UN);
fclose($fp);


return $data;
}

Nem mais. Dependendo daquilo para que precisamos do lock/wtv, até podemos ter soluções mais eficientes que as que apresentaste.

Isso não quer dizer que não se vá usar no futuro.

Então mas o problema da solução apresentada não era com o flock?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites
É para um caso real. Está-se a evitar o uso de um SGBD

Ah! Hehe... Bom, de qualquer modo esse método funciona, e se fizerem um profiling verão que é até bastante rápido. O bottleneck está no último do-while como vos parecerá óbvio. Mas não será nada de extraordinário comparado com os métodos de lock usados em muitos RDBMS'. A operação mais lenta é sempre o fopen(). Tudo o resto é peanuts.

Já agora... nem mesmo o SQLite? "Vá lá. Não é por mim, é por ela..."

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Então mas o problema da solução apresentada não era com o flock?

Sem recorrer a um RDBMS, é o melhor que se arranja, e para os requests que tem, não precisa de mais. No entanto, a solução óptima no nosso caso era dar uso ao MySQL.
0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Sem recorrer a um RDBMS, é o melhor que se arranja, e para os requests que tem, não precisa de mais. No entanto, a solução óptima no nosso caso era dar uso ao MySQL.

Então e a segunda solução do vitortomaz?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Então e a segunda solução do vitortomaz?

É essa a única solução possível. Ou outra coisa qualquer que implemente um check ao estado actual do ficheiro antes de fazer o write. Mas nunca o flock(). O meu conselho é que esqueçam o flock() até algo de mais consistente ser desenvolvido. Existem no PEAR (se não me falha a memória) outras implementações de acesso ao filesystem que resolvem o problema substituindo o flock com as suas próprias implementações. Essa seria uma terceira hipótese. Mas... volto a repetir, flock não.

Na realidade existe ainda um outro problema com o flock que tem a ver com o facto de poderem estar a utilizar uma API com suporte multithreading como o ISAPI. Nesse caso se dois utilizadores requerem um lock, cada um na sua thread, ambos conseguem! O que basicamente derrota toda a idea por detrás de um locking system. Isto não acontece no Linux. Mas acontce no Windows pelo menos até ao Server 2003. Daí par a frente não sei.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

if ((@filemtime($fp[1]) == $fp[3]) or ($fp[4]==true and !file_exists($fp[1])) or $fp[5]==true) {
         unlink($fp[1]);
         rename($tempfilename, $fp[1]);
}

na parte de em se muda o ficheiro temporário para o original tive que meter um unlink antes do rename porque em windows não se pode renomear para um ficheiro que já exista...

agora a dúvida é a seguinte, entre o if o unlink e o rename pode acontecer muita coisa... logo isto não está thread safe certo?  :wallbash:

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

bom... não pode acontecer nada se o apache (já agora, é Apache?) não estiver a correr em multithread mode.

O php é thread safe no core em parte. Isto ainda é assunto que gera muita discussão e está longe de estar acabado. Mas o importante a reter aqui é que se o Apache não estiver configurado em modo multithread, estás à vontade. Nada vai acontecer entre o unlink e o rename porque todo o script é atómico.

Se o Apache estiver configurado em modo multithread, poderá haver problemas se, e só se, existirem bugs na implementação do mecanismo de thread safety do PHP para o teu caso em específico, uma vez que - volto a frisar - o PHP é em principio thread safe.

Mas uma vez que estás a trabalhar com ficheiros (sobre os quais o PHP não tem nenhum controlo) o truque, se quiseres garantir que não vais ter problemas, é checar o resultado do fopen() e se for FALSE, significa que não conseguiu abrir o ficheiro. Nesse caso, o meu conselho é matares o script. O utilizador fará um refresh e nem dá conta. Mas verdade seja dita... se vires um FALSE, a culpa não é do script. Mas sim do PHP que nunca deveria intrometer uma thread dentro daquele if()

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Pelo que percebi querem apenas saber os downloads a revista

a minha sugestão é a seguinte, em vez de abrir um ficheiro, ler o que lá esta dentro, incrementar e gravar novamente o ficheiro é criar um ficheiro vazio numa pasta por cada acesso e depois contar o numero de ficheiros

não testei, mas é algo assim

$d = 'ed15/';
$file = md5(microtime()) . '.cnt';

//criar ficheiro
touch($d . $file );

//total
echo count ( glob($d.'*.cnt') ) ;

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Pelo que percebi querem apenas saber os downloads a revista

a minha sugestão é a seguinte, em vez de abrir um ficheiro, ler o que lá esta dentro, incrementar e gravar novamente o ficheiro é criar um ficheiro vazio numa pasta por cada acesso e depois contar o numero de ficheiros

não testei, mas é algo assim

$d = 'ed15/';
$file = md5(microtime()) . '.cnt';

//criar ficheiro
touch($d . $file );

//total
echo count ( glob($d.'*.cnt') ) ;

Has-de experimentar encher uma pasta com mais de 2000 ficheiros e usar a função pra tirar a lista de ficheiros. Vais ficar parvo com o tempo que demora :)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

mas estão a pensar em mostras as estatísticas constantemente ?

olha que aqui no meu chaço com 2000 ficheiros não achei nada demais

$start = microtime(true);
$total = count( glob('test/*.cnt'));
$end = microtime(true);

printf("files: %s <br> start: %s <br> end %s <br> time %s",$total, $start, $end, ($end-$start));

files: 2000

start: 1217900452.6719

end 1217900453.0082

time 0.33635997772217

mas obviamente que está não é uma solução "by the book"

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

mas obviamente que está não é uma solução "by the book"

Mas é fiável ao nível da concorrência ;)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

e é esse o problema que estão a ter, como disse se não estão a pensar em mostrar as estatísticas em cada página do site este método tem bem mais performance do que o que estão usar actualmente

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Crie uma conta ou ligue-se para comentar

Só membros podem comentar

Criar nova conta

Registe para ter uma conta na nossa comunidade. É fácil!


Registar nova conta

Entra

Já tem conta? Inicie sessão aqui.


Entrar Agora