RabbitMQ: identify messages from same prefetch - rabbitmq

My producer looks like this:
$connection = new AMQPStreamConnection(
$settings['amqp']['host'],
$settings['amqp']['port'],
$settings['amqp']['username'],
$settings['amqp']['password']
);
$channel = $connection->channel();
$channel->queue_declare($settings['amqp']['queue'], false, true, false, false);
$msg = array();
$msg['time'] = time();
$msg = new AMQPMessage(json_encode($msg), array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT));
$channel->basic_publish($msg, '', $settings['amqp']['queue']);
$channel->close();
$connection->close();
And my consumer looks like this:
$connection = new AMQPStreamConnection(
$settings['amqp']['host'],
$settings['amqp']['port'],
$settings['amqp']['username'],
$settings['amqp']['password']
);
$channel = $connection->channel();
$channel->queue_declare($settings['amqp']['queue'], false, true, false, false);
$callback = function($message) {
//echo $message->body . "\n";
//echo $message->delivery_info['delivery_tag'] . "\n";
var_dump($message) . "\n";
$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
};
$channel->basic_qos(null, 30, null);
$channel->basic_consume($settings['amqp']['queue'], '', false, false, false, false, $callback);
try {
while(count($channel->callbacks)) {
$channel->wait();
}
} catch (Exception $e){
die();
}
Since I'm setted basic_qos to null, 30, null my consumers retrieve blocks of 30 messages from the cluster.
My goal is identify these 30 as a group. And the next 30 as a diferent group.
Something like that:
ATTENTION: Example PHP + BASH !!!
$callback = function($message) {
mkdir -p /tmp/$message->delivery_info['prefetch_group']
echo $message->body > /tmp/$message->delivery_info['prefetch_group']/$message->delivery_info['delivery_tag']
};
At the end. I will have one dir for each block with the content of the messages.
Of course $message->delivery_info['prefetch_group'] doesn't exist. And I didn't found anything usefull into $message
It would be ideal send the basis_ack for each block (30 messages) only if all the process is done.
Any idea to deal with this?

There is nothing stopping you from processing 30 messages before receiving another 30 messages.
Wait to send the acknowledgement until after you have processed the 30th message
Be sure to set the multiple flag to "true"; this will acknowledge all messages received up to the 30th message
Obviously you'll need to keep the messages in the order they were received (they come one at a time from the broker). This can be done by sorting on DeliveryTag.
As a caveat, if for example the message processor fails/crashes with the 30th message, and you don't acknowledge the other 29, then you're going to end up getting those re-delivered to you, so this may be something to consider in your design.
Also note from the spec:
The server may send less data in advance than allowed by the client's specified prefetch windows but it MUST NOT send more.

After some more research I have done this.
Using basic_get instead basic_consume like that:
CODE NOT PROBED !!!. Writed like an example.
16 $connection = new AMQPStreamConnection(
17 $settings['amqp']['host'],
18 $settings['amqp']['port'],
19 $settings['amqp']['username'],
20 $settings['amqp']['password']
21 );
22
23 $channel = $connection->channel();
24 $channel->queue_declare($settings['amqp']['queue'], false, true, false, false);
25
26 $messages = Array();
27 for ($i=0; $i < 30; $i++) {
28 $messages[] = $channel->basic_get($settings['amqp']['queue']);
29 }
30
31 $now = time();
32 foreach($messages as $message) {
33 // DO WHATEVER YOU WANT WITH EACH MSG
34 // system('mkdir -p /tmp/' . $now);
// system('echo ' . $message->body . '> /tmp/' . $now . '/' . $message->delivery_info['delivery_tag']);
35 echo $message->body . "\n";
36 }
37
38 // system('tar /tmp/' . $now . '.gz cvfz /tmp/' . $now);
39
40 foreach($messages as $message) {
41 $channel->basic_ack($message->delivery_info['delivery_tag']);
42 }
43
44 $channel->close();
45 $connection->close();
This in only a POC. Would be necessary:
Be sure there is a message before push to $messages (Line 28)
Control many possible errors.
Daemonize the consumer in some way.
I need read more about basic_get vs basic_consume but this solutions seems good to me.

Related

How to consume all messages from a RabbitMQ queue in batches?

My RabbitMQ consumer has to process messages in fixed size batches with a prefetch count of 15. This is limited to 15 in order to match AWS SES email send rate - the consumer process has to issue SES API requests in parallel. What would be the best way of dealing with a partial final batch, i.e. one that's left with fewer than 15 messages.
Any new messages are added to the consumer's batch array. When the batch size is achieved, the batch is processed and an acknowledgement is sent back for all messages in the batch.
The 'leftover messages' in the final batch must be processed in my scenario before the 10 second connection timeout comes into effect. Is there a way to implement a timeout on the callback function so that, if for a certain period the number of messages received is less than the prefetch count, the remaining messages in consumer's temporary batch array are processed and acknowledged. It's worth mentioning that no new messages are published to the queue while the consumer process is executing. Thanks in advance.
$connection = new AMQPStreamConnection(HOST, PORT, USER, PASS);
$channel = $connection->channel();
$channel->queue_declare('test_queue', true, false, false, false);
$channel->basic_qos(null, 15, null);
$callback = function($message){
Ack_Globals::$msg_batch[] = $message;
Ack_Globals::$msg_count++;
$time_diff = Ack_Globals::$cur_time-Ack_Globals::$lbatch_recieved;
if(sizeof(Ack_Globals::$msg_batch) >= 15){
$time_end = microtime(true);
Ack_Globals::$lbatch_recieved = $time_end;
Ack_Globals::$batch_count = Ack_Globals::$batch_count + 1;
//Calculate the average time to create this batch
Ack_Globals::$bgen_time_avg = ($time_end - Ack_Globals::$time_start)/Ack_Globals::$batch_count;
//Process this batch
/* Process */
//Acknowledge this batch
$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag'], true);
echo "\nMessage Count: ".Ack_Globals::$msg_count;
echo "\nSize of Array: ".sizeof(Ack_Globals::$msg_batch);
echo "\nLast batch received: ".Ack_Globals::$lbatch_recieved;
echo "\nBatch ".Ack_Globals::$batch_count." processed.";
echo "\nAverage batch generation time: ". Ack_Globals::$bgen_time_avg;
//Clear the batch array
Ack_Globals::$msg_batch = array();
}else{}
};
if ((Ack_Globals::$batch_count === 0) && (Ack_Globals::$msg_count === 0)){
//initialise the timer
Ack_Globals::$time_start = microtime(true);
}
$channel->basic_consume('int_surveys', '', false, false, false, false, $callback);
while ($channel->is_consuming()){
Ack_Globals::$cur_time = AMQPChannel::$current_time;
$channel->wait(null, false, 10);
}
$channel->close();
$connection->close();

Cannot use threads to insert data to PostgreSQL with DBIish. What's going wrong?

Edit: This was solved by moritz. I've added a note to the code on the line that's wrong.
My application is a web server talking to a game client. The server is multithreaded, which Postgres allows. When loading client data into the database, I noticed parallel requests fail with several different errors, none of which make sense to me.
This short-ish test case dumps a nested hash into the database. When run without start, it works perfectly. When run with threads, it almost always gives one or more of the following errors:
DBDish::Pg: Error: (7) in method prepare at
D:\rakudo\share\perl6\site\sources\BAD7C1548F63C7AA7BC86BEDDA0F7BD185E141AD
(DBDish::Pg::Connection) line 48 in block at testcase.p6 line 62
in sub add-enum-mappings at testcase.p6 line 59 in block at
testcase.p6 line 91
DBDish::Pg: Error: ERROR: prepared statement
"pg_3448_16" already exists (7) in method prepare at
D:\rakudo\share\perl6\site\sources\BAD7C1548F63C7AA7BC86BEDDA0F7BD185E141AD
(DBDish::Pg::Connection) line 46 in block at testcase.p6 line 62
in sub add-enum-mappings at testcase.p6 line 59 in block at
testcase.p6 line 91
DBDish::Pg: Error: Wrong number of arguments to
method execute: got 1, expected 0 (-1) in method enter-execute at
D:\rakudo\share\perl6\site\sources\65FFB78EFA3030486D1C4D339882A410E3C94AD2
(DBDish::StatementHandle) line 40 in method execute at
D:\rakudo\share\perl6\site\sources\B3190B6E6B1AA764F7521B490408245094C6AA87
(DBDish::Pg::StatementHandle) line 52 in sub add-enum-mappings at
testcase.p6 line 54 in block at testcase.p6 line 90
message type 0x31 arrived from server while idle
message type 0x5a arrived from server while idle
message type 0x74 arrived from server while idle
message type 0x6e arrived from server while idle
message type 0x5a arrived from server while idle
Here's the code. (If you choose to run it, remember to set the right password. It creates/manipulates a table called "enummappings", but does nothing else.) The meat is in add-enum-mappings(). Everything else is just setup. Oh, and dbh() creates a separate DB connection for each thread. This is necessary, according to the PostgreSQL docs.
#!/usr/bin/env perl6
use DBIish;
use Log::Async;
my Lock $db-lock;
my Lock $deletion-lock;
my Lock $insertion-lock;
INIT {
logger.send-to($*ERR);
$db-lock .= new;
$deletion-lock .= new;
$insertion-lock .= new;
}
# Get a per-thread database connection.
sub dbh() {
state %connections;
my $dbh := %connections<$*THREAD.id>; # THIS IS WRONG. Should be %connections{$*THREAD.id}.
$db-lock.protect: {
if !$dbh.defined {
$dbh = DBIish.connect('Pg', :host<127.0.0.1>, :port(5432), :database<postgres>,
:user<postgres>, :password<PASSWORD>);
}
};
return $dbh;
}
sub create-table() {
my $name = 'enummappings';
my $column-spec =
'enumname TEXT NOT NULL, name TEXT NOT NULL, value INTEGER NOT NULL, UNIQUE(enumname, name)';
my $version = 1;
my $sth = dbh.prepare("CREATE TABLE IF NOT EXISTS $name ($column-spec);");
$sth.execute;
# And add the version number to a version table:
dbh.execute:
"CREATE TABLE IF NOT EXISTS tableversions (name TEXT NOT NULL UNIQUE, version INTEGER NOT NULL);";
$sth = dbh.prepare:
'INSERT INTO tableversions (name, version) VALUES (?, ?)
ON CONFLICT (name)
DO
UPDATE SET version = ?;';
$sth.execute($name, $version, $version);
}
sub add-enum-mappings($enumname, #names, #values --> Hash) {
$deletion-lock.protect: {
my $sth = dbh.prepare('DELETE FROM enummappings WHERE enumname = ?;');
$sth.execute($enumname);
};
my #rows = (^#names).map: -> $i {$enumname, #names[$i], #values[$i]};
info "Inserting #rows.elems() rows...";
$insertion-lock.protect: {
my $sth = dbh.prepare('INSERT INTO enummappings (enumname,name,value) VALUES '~
('(?,?,?)' xx #rows.elems).join(',') ~ ';');
$sth.execute(#rows>>.list.flat);
};
return %(status => 'okay');
}
# Create a bunch of long enums with random names, keys, and values.
sub create-enums(--> Hash[Hash]) {
my #letters = ('a'..'z', 'A'..'Z').flat;
my Hash %enums = ();
for ^36 {
my $key = #letters.pick(10).join;
for ^45 {
my $sub-key = #letters.pick(24).join;
%enums{$key}{$sub-key} = (0..10).pick;
}
}
return %enums;
}
sub MAIN() {
create-table;
await do for create-enums.kv -> $enum-name, %enum {
start {
add-enum-mappings($enum-name, %enum.keys, %enum.values);
CATCH { default { note "Got error adding enum: " ~ .gist; } }
};
}
}
I'm on Windows 10, with a 8-core computer. I know I could insert the data single-threadedly, but what if the game gets a hundred connections at once? I need to fix this for good.
I suspect your problem is here:
my $dbh := %connections<$*THREAD.id>;
The %hash<...> syntax is only for literals. You really need to write %connections{$*THREAD.id}.
With your error in place, you have just one DB connection that's shared between all threads, and I guess that's what DBIish (or the underlying postgresql C client library) is unhappy about.

Predis error when using pipeline with redis-cluster

i try to add a key-value-pair in my redis-cluster and set expire for the new key in one pipeline. Every time i get an error that the key is moved, but i think that Predis should follow the MOVED-statement like without pipelining.
Isn't it possible to make an expire-call in the pipe? I'm using Predis 1.0.2-dev
with redis_version: 3.0.2
This works:
$parameters = ['tcp://10.9.200.51:47801', 'tcp://10.9.200.52:47801', 'tcp://10.9.200.53:47801', 'tcp://10.9.200.54:47801'];
$options = ['cluster' => 'redis'];
$redis = new Predis\Client($parameters, $options);
for($i = 0; $i < 10; $i++)
{
$rand = mt_rand(1111111,9999999);
$k = 'test_'.$rand;
try{
$redis->set($k, 1);
$redis->expire($k, 10);
}
catch(Exception $ex)
{
print_r($ex);
}
}
?>
This does not work:
$parameters = ['tcp://10.9.200.51:47801', 'tcp://10.9.200.52:47801', 'tcp://10.9.200.53:47801', 'tcp://10.9.200.54:47801'];
$options = ['cluster' => 'redis'];
$redis = new Predis\Client($parameters, $options);
$pipe = $redis->pipeline();
for($i = 0; $i < 10; $i++)
{
$rand = mt_rand(1111111,9999999);
$k = 'test_'.$rand;
try{
$pipe->set($k, 1);
$pipe->expire($k, 10);
}
catch(Exception $ex)
{
print_r($ex);
}
}
$pipe->execute();
?>
I get this error:
PHP Fatal error: Uncaught exception 'Predis\Response\ServerException' with message 'MOVED 7276 10.9.200.61:47902' in /var/www/predis_test/Predis/Pipeline/Pipeline.php:105
Stack trace:
#0 /var/www/predis_test/Predis/Pipeline/Pipeline.php(149): Predis\Pipeline\Pipeline->exception(Object(Predis\Connection\Aggregate\RedisCluster), Object(Predis\Response\Error))
#1 /var/www/predis_test/Predis/Pipeline/Pipeline.php(168): Predis\Pipeline\Pipeline->executePipeline(Object(Predis\Connection\Aggregate\RedisCluster), Object(SplQueue))
#2 /var/www/predis_test/Predis/Pipeline/Pipeline.php(217): Predis\Pipeline\Pipeline->flushPipeline()
#3 /var/www/predis_test/lasttest.php(31): Predis\Pipeline\Pipeline->execute()
#4 {main}
thrown in /var/www/predis_test/Predis/Pipeline/Pipeline.php on line 105
EDIT: Seems that pipelining doens't work on redis-cluster. I get the same error when I remove the expire call and have only the set call in a pipe.
Predis pipeline requires the keys in the same SLOT in cluster(for cluster support auto sharding), which makes it hard for client to handle received moved message from redis cluster server.
More details please refer to the following issue and discussion:
Issue: Pipelining with redis-cluster throws exception #267
Discussion: Redis Cluster with Pipeline
The Conclusion is: do not use pipeline under redis cluster.

i am having a issue with json codeigniter rest its not closing the tag

i am having a problem with json codeigniter rest
i am making this call to the server and the problem its that its not closing the json tags
s, USA","clientUID":"7","email":null,"idipad":"2","dateModified":null},{"id":"19","uid":null,"name":"Wayne Corporation, Inc.","phone":"932345324","address":"Second st. 312, Gotham City","clientUID":"7","email":"waynecorp#gmail.com","idipad":"1","dateModified":null}]
its missing the final }
this is the code that creates the response :
$this->response(array('login'=>'login success!','user_admin_id'=>$user_id,'client'=>$client,'users'=>$users,'projects'=>$projects,'plans'=>$plans,'meetings'=>$meetings,'demands'=>$demands,'tasks'=>$tasks,'presences'=>$presences,'contractors'=>$contractors,'companies'=>$companies), 200);
this is the client call using curl :
$this->curl->create('http://dev.onplans.ch/onplans/index.php/api/example/login/format/json');
// Option & Options
$this->curl->option(CURLOPT_BUFFERSIZE, 10);
$this->curl->options(array(CURLOPT_BUFFERSIZE => 10));
// More human looking options
$this->curl->option('buffersize', 10);
// Login to HTTP user authentication
$this->curl->http_login('admin', '1234');
// Post - If you do not use post, it will just run a GET request
//$post = array('remember'=>'true','email'=>'admin.architect#onplans.ch','password'=>'password');
$post = array('remember'=>'true','email'=>'admin.architect#onplans.ch','password'=>'password');
$this->curl->post($post);
// Cookies - If you do not use post, it will just run a GET request
$vars = array('remember'=>'true','email'=>'manuel#ffff.com','password'=>'password');
$this->curl->set_cookies($vars);
// Proxy - Request the page through a proxy server
// Port is optional, defaults to 80
//$this->curl->proxy('http://example.com', 1080);
//$this->curl->proxy('http://example.com');
// Proxy login
//$this->curl->proxy_login('username', 'password');
// Execute - returns responce
echo $this->curl->execute();
// Debug data ------------------------------------------------
// Errors
$this->curl->error_code; // int
$this->curl->error_string;
print_r('error :::::LOGINN REMOTE:::::'.$this->curl->error_string);
// Information
$this->curl->info; // array
print_r('info :::::::::::::'.$this->curl->info);
the response belong to the rest api codeigniter from phil
/**
* Response
*
* Takes pure data and optionally a status code, then creates the response.
*
* #param array $data
* #param null|int $http_code
*/
public function response($data = array(), $http_code = null)
{
global $CFG;
// If data is empty and not code provide, error and bail
if (empty($data) && $http_code === null)
{
$http_code = 404;
// create the output variable here in the case of $this->response(array());
$output = NULL;
}
// If data is empty but http code provided, keep the output empty
else if (empty($data) && is_numeric($http_code))
{
$output = NULL;
}
// Otherwise (if no data but 200 provided) or some data, carry on camping!
else
{
// Is compression requested?
if ($CFG->item('compress_output') === TRUE && $this->_zlib_oc == FALSE)
{
if (extension_loaded('zlib'))
{
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) AND strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
{
ob_start('ob_gzhandler');
}
}
}
is_numeric($http_code) OR $http_code = 200;
// If the format method exists, call and return the output in that format
if (method_exists($this, '_format_'.$this->response->format))
{
// Set the correct format header
header('Content-Type: '.$this->_supported_formats[$this->response->format]);
$output = $this->{'_format_'.$this->response->format}($data);
}
// If the format method exists, call and return the output in that format
elseif (method_exists($this->format, 'to_'.$this->response->format))
{
// Set the correct format header
header('Content-Type: '.$this->_supported_formats[$this->response->format]);
$output = $this->format->factory($data)->{'to_'.$this->response->format}();
}
// Format not supported, output directly
else
{
$output = $data;
}
}
header('HTTP/1.1: ' . $http_code);
header('Status: ' . $http_code);
// If zlib.output_compression is enabled it will compress the output,
// but it will not modify the content-length header to compensate for
// the reduction, causing the browser to hang waiting for more data.
// We'll just skip content-length in those cases.
if ( ! $this->_zlib_oc && ! $CFG->item('compress_output'))
{
header('Content-Length: ' . strlen($output));
}
exit($output);
}
This answer was referenced from Github issue. Also raised by Pedro Dinis, i guest.
I met this problem today and take me long hours to search for the solution. I share here with hope to help someone like me.
The key is to replace around line 430 in the library file: REST_Controller.php :
header('Content-Length: ' . strlen($output));
by
header('Content-Length: ' . strlen("'".$output."'"));
UPDATE: The problem was solved here
Or you can just comment out the code, it will run fine. :)

Help creating selective IF statement

I have this code that sends a text message to your mobile phone...
$text = fopen("../data/textmembers/registry.txt","r") or die("couldent open file");
while (!feof($text)) {
$name = fgets($text);
$number = fgets($text);
$carrier = fgets($text);
$date = fgets($text);
$line = fgets($text);
$content = $_POST['message'];
$message .= $content;
$message .= "\n";
$number = trim($number);
mail($number . "#vtext.com", "SGA Event Alert", $message, "SGA");
Header("Location: mailconf.php");
everything works fine.. Here is my question, if you look at where I have "#vtext.com"
as you may or may not know, each carrier has its own extension, verizon is #vtext.com, at&t is #txt.att.net. I need to take the feed from "$carrier" decide what carrier it is, and then assign the extension to it...
I thought an ifelse would work, but I am not good with if statements...
the user's choices are
Verizon = 1234567890#vtext.com
AT&T = 1234567890#txt.att.net
T-mobile = #tmomail.net
Nextel = #messaging.nextel.com
thanks guys!!
$carriers = array(
"verizon" => "vtext.com",
"at&t" => "txt.att.net",
"t-mobile" => "tmomail.net",
"nextel" => "messaging.nextel.com"
);
Then, you get that value by looking up the key:
print $carriers[strtolower($carrier)];
If $carrier is "Nextel," "messaging.nextel.com" will be returned.
Probably better than using an if statement would be using a switch statement.
Have a look at the section of the PHP manual that deals with the switch statement.