Skip to content
Browse files

Initial import. Works.

  • Loading branch information...
0 parents commit 89e5ef353f4b5beb84c14c0a550393f07772b037 @cosimo committed with Sep 22, 2011
Showing with 357 additions and 0 deletions.
  1. +26 −0 README.md
  2. +72 −0 bin/stress.pl
  3. +174 −0 lib/TicketServer.pm
  4. +8 −0 sql/tickets.sql
  5. +77 −0 t/basic.t
26 README.md
@@ -0,0 +1,26 @@
+Tickets server
+=========================
+
+A sample implementation hacked together in a couple of hours
+in Perl + MySQL of the Flickr tickets server, as explained
+in this [blog post](http://code.flickr.com/blog/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/).
+
+Assumptions
+-----------
+
+* MySQL 5.1+ is installed locally, listening on `localhost:3306`
+* Perl 5.8+ is installed, along with DBI (`apt-get install libdbi-perl`)
+
+How to use this
+---------------
+
+* Basic sql schema is found in `sql/tickets.sql`
+* To test basic sanity, after setting up the db, run: `prove -Ilib -v`
+* To stress test the tickets db, run: `perl bin/stress.pl`
+
+Contacts
+--------
+
+Email: cosimo@cpan.org
+Twitter: @cstrep
+
72 bin/stress.pl
@@ -0,0 +1,72 @@
+#!/usr/bin/env perl
+#
+# Long-term stress-test for the ticket db
+#
+
+use strict;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$FindBin::Bin/../lib";
+
+use Time::HiRes;
+use TicketServer;
+
+my $host = 'localhost';
+my $port = 3306;
+my $dbname = 'tickets';
+my $user = 'root';
+my $password = '';
+
+my $dsn = "DBI:mysql:dbname=$dbname;host=$host;port=$port";
+
+my $tm = TicketServer->new({
+ dsn => $dsn,
+ user => $user,
+ password => $password,
+});
+
+my @seq = qw(s1 s2 s3 s4 s5 s6);
+
+for (@seq) {
+ $tm->drop_sequence($_);
+ $tm->create_sequence($_);
+}
+
+my $ticks =
+my $gets =
+my $done =
+my $total = 0;
+
+$SIG{INT} = sub { $done = 1 };
+$| = 1;
+
+my $start_time = [Time::HiRes::gettimeofday()];
+my %values;
+
+while (not $done) {
+ $ticks++;
+
+ my $random_seq = $seq[int rand(@seq)];
+ my $val = $tm->next_val($random_seq);
+ $gets++;
+ $total++;
+
+ $values{$random_seq} = $val;
+
+ if ($ticks % 5000 == 0) {
+ my $elapsed = Time::HiRes::tv_interval($start_time);
+ my $persec = int ($gets / $elapsed);
+ $start_time = [Time::HiRes::gettimeofday()];
+ $gets = 0;
+ print "ops:$total ($persec/s) [";
+
+ for (sort keys %values) {
+ print "$_:$values{$_} ";
+ }
+
+ print "] \r";
+ }
+
+}
+
174 lib/TicketServer.pm
@@ -0,0 +1,174 @@
+package TicketServer;
+
+use strict;
+use warnings;
+
+use Carp ();
+use DBI;
+
+sub new {
+ my ($class, $args) = @_;
+
+ $class = ref $class || $class;
+ $args ||= {};
+
+ my $self = {
+ _dbh => undef,
+ _dbname => undef,
+ _dsn => $args->{dsn},
+ _user => $args->{user},
+ _password => $args->{password},
+ };
+
+ bless $self, $class;
+}
+
+sub connect {
+ my ($self, $dsn, $user, $pass) = @_;
+
+ $dsn ||= $self->{_dsn};
+ $user ||= $self->{_user};
+ $pass ||= $self->{_password};
+
+ if (! $dsn or ! $user) {
+ Carp::croak("No DSN or user. Can't connect to tickets database!");
+ }
+
+ my $dbh = DBI->connect($dsn, $user, $pass, {
+ RaiseError => 1,
+ PrintError => 1
+ })
+ or return;
+
+ if (! $dbh->ping()) {
+ return;
+ }
+
+ my $dbname;
+ if ($dsn =~ m{DBI : [^:]+ : (?:dbname=) (\w+)}ix) {
+ $dbname = $1;
+ }
+ else {
+ Carp::croak("Couldn't get database name from DSN $dsn");
+ }
+
+ $self->{_dbname} = $dbname;
+
+ return $self->{_dbh} = $dbh;
+}
+
+sub drop_sequence {
+ my ($self, $name) = @_;
+ return unless defined $name && $name ne '';
+
+ $name =~ s{\W}{}g ;
+
+ my $sql = "DROP TABLE $name";
+ my $dropped = 0;
+
+ $dropped = eval {
+ my $dbh = $self->get_dbh();
+ my $sth = $dbh->prepare($sql);
+ $dropped = $sth->execute();
+ $sth->finish();
+ return $dropped;
+ };
+
+ return $dropped;
+}
+
+sub reset_sequence {
+ my ($self, $name, $start_value) = @_;
+
+ $self->drop_sequence($name);
+ $self->create_sequence($name, $start_value);
+}
+
+sub get_dbh {
+ my ($self) = @_;
+
+ my $cached_dbh = $self->{_dbh};
+ $cached_dbh = eval {
+ $cached_dbh
+ and ref $cached_dbh
+ and $cached_dbh->FETCH('Active')
+ and $cached_dbh->ping()
+ and $cached_dbh
+ };
+
+ if (! $cached_dbh) {
+ my $new_dbh = $self->connect();
+ $self->{_dbh} = $cached_dbh = $new_dbh;
+ }
+
+ return $cached_dbh;
+}
+
+sub create_sequence {
+ my ($self, $name, $start_value) = @_;
+
+ return unless defined $name && $name ne '';
+
+ if (! defined $start_value || $start_value eq "") {
+ $start_value = 1;
+ }
+
+ $name =~ s{\W}{}g;
+ $start_value =~ s{\D}{}g;
+
+ my $sql = <<SQL;
+CREATE TABLE IF NOT EXISTS $name (
+ id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ stub char(1) NOT NULL UNIQUE DEFAULT ''
+) ENGINE=MyISAM AUTO_INCREMENT=$start_value CHARACTER SET='UTF8'
+SQL
+ my $created = 0;
+
+ eval {
+ my $dbh = $self->get_dbh();
+ my $sth = $dbh->prepare($sql);
+ $created = $sth->execute();
+ $sth->finish();
+ $created;
+ } or do {
+ Carp::croak("New sequence $name not created: $@");
+ };
+
+ return 1;
+}
+
+sub next_val {
+ my ($self, $sequence) = @_;
+
+ my $dbh = $self->get_dbh();
+ if (! $dbh) {
+ Carp::croak("Can't get next value: $!");
+ }
+
+ $sequence =~ s{\W}{}g;
+
+ my $ok = 0;
+ my $tries = 3;
+ my $sth;
+
+ while (--$tries) {
+ $sth = $dbh->prepare("REPLACE INTO $sequence (stub) VALUES ('a')");
+ $ok = $sth->execute();
+ if (! $ok) {
+ warn("Attempt to get next value from sequence '$sequence' FAILED: $!");
+ }
+ last if $ok;
+ }
+
+ if (! $ok) {
+ Carp::croak("PANIC: Couldn't get next value from sequence '$sequence'");
+ }
+
+ my $dbname = $self->{_dbname};
+ my ($next_val) = $dbh->last_insert_id(undef, $dbname, $sequence, 'id');
+ $sth->finish() if $sth;
+
+ return $next_val;
+}
+
+1;
8 sql/tickets.sql
@@ -0,0 +1,8 @@
+-- For MySQL
+
+DROP TABLE IF EXISTS users_sequence;
+CREATE TABLE users_sequence (
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ stub char(1) NOT NULL UNIQUE DEFAULT ''
+) ENGINE=MyISAM AUTO_INCREMENT=80000000000 DEFAULT CHARSET=utf8;
+
77 t/basic.t
@@ -0,0 +1,77 @@
+#!/usr/bin/env perl
+#
+# Basic test
+#
+
+use strict;
+use warnings;
+use TicketServer;
+use Test::More;
+
+my $host = 'localhost';
+my $port = 3306;
+my $dbname = 'tickets';
+my $dsn = "DBI:mysql:dbname=$dbname;host=$host;port=$port";
+my $user = 'root';
+my $password = '';
+
+my $tm = TicketServer->new({
+ dsn => $dsn,
+ user => $user,
+ password => $password,
+});
+
+ok $tm, 'TicketServer object created';
+
+my $seq = 'users_sequence';
+ok $tm->reset_sequence($seq, 800_000_000_000),
+ "reset_sequence() with value";
+
+my $count = 10;
+while ($count--) {
+ my $next = $tm->next_val($seq);
+ is $next => 800_000_000_009 - $count,
+ "reset_sequence() with a value works [$next]";
+}
+
+ok $tm->reset_sequence($seq), "reset_sequence() without value";
+
+$count = 10;
+
+while ($count--) {
+ my $next = $tm->next_val($seq);
+ is $next => 10 - $count,
+ "reset_sequence() without values + next_val() works [$next]";
+ #diag $next;
+}
+
+ok $tm->reset_sequence($seq, 1000), "reset_sequence() with a lower value";
+
+$count = 10;
+
+while ($count--) {
+ my $next = $tm->next_val($seq);
+ is $next => 1009 - $count,
+ "reset_sequence() again with a value [$next]";
+ #diag $next;
+}
+
+$seq = 'photos';
+
+ok $tm->create_sequence($seq, 500_000_000_000_000),
+ "create a new sequence with a starting value";
+
+$count = 100;
+
+while ($count--) {
+ my $next = $tm->next_val($seq);
+ is $next => 500_000_000_000_099 - $count,
+ "reset_sequence() again with a value [$next]";
+ #diag $next;
+}
+
+ok $tm->drop_sequence($seq),
+ "drop an existing sequence";
+
+done_testing;
+

0 comments on commit 89e5ef3

Please sign in to comment.
Something went wrong with that request. Please try again.