lancew@cpan.org

About Me

Lance Wicks,

Development team leader at www.cv-library.co.uk

 

100,000+ jobs from 10,000+ companies, 10,000,000+ users, 2,000,000+ job applications per month.

 

 

Perl 5 code base started in 2000.

Yes, we are hiring!

http://www.cv-library.co.uk/yapceu

Net::StatsdToggle
Scientist

Our Situation

16 year old code base, Perl5, fast growth, lots of new features, 400,000 lines of code.

 

Refactoring is essential, but difficult.

 

Zero tolerance to breaking the site, or negatively impacting performance.

 

Our Refactoring Rules

  • If you can't test it, don't refactor.


  • If you can't measure it, don't refactor.


  • If you can't turn it on (and off!), don't refactor.

Our Refactoring Rules

  • If you can't test it, don't refactor.
                Test::More
     
  • If you can't measure it, don't refactor.
    ​            Net::Statsd
     
  • If you can't turn it on (and off!), don't refactor.
                Toggle

Test::More

 

If you can't test it, don't refactor it.

 

 

EXODIST

 

use Test::More; 

my $transactions_via_old = $candidate->transactions;
my $transactions_via_new = $candidate->transactions_new;

is_deeply(
    $transactions_via_old, 
    $transactions_via_new,
    'Old and new methods return same data structures'
);

done_testing;

Net::Statsd

 

If you can't measure it, don't refactor it.

 

 

COSIMO

 

use Time::HiRes;
my $start_time = [ Time::HiRes::gettimeofday ];

for my $candidate (@active_candidates) {
    # Expensive transaction in loop of large data set
    update_with_new_total($candidate);
    
    Net::Statsd::inc('candidates.updated_with_new_totals');
}


Net::Statsd::timing(
    'candidates.new_totals',
    Time::HiRes::tv_interval($start_time) * 1000
);

Toggle

 

If you can't turn it on AND off, don't refactor it.

 

 

CVLIBRARY

use Toggle;
use Redis;

my $redis = Redis->new;
my $toggle = Toggle->new( storage => $redis );

$toggle->define_group( staff => sub {
    my $user = shift;
    return $user->has_role('staff');
});

if ( $toggle->is_active(test_new_query => $user) ) {
    $db_repo->get_transactions(user => $user)
}

Scientist

 

 

Bringing it all together.

 

LANCEW





# TODO: Lets talk about that later. :-)

How we got here.

Step 1: Automated testing

Unit, integration/service and end to end tests all run by Jenkins on every commit.

Step 1: Automated testing

use CVLibrary::SalaryConverter 'convert_salary';
use Test::Fatal;
use Test::More;

sub is_salary_converted_from_day_month : Test {
    is convert_salary( from => 'day', to => 'month', value => 75 ), 1629;
}

sub is_salary_converted_from_month_to_year : Test {
    is convert_salary( from => 'month', to => 'annum', value => 1629 ), 19548;
}

sub is_salary_converted_from_year_to_week : Test {
    is convert_salary( from => 'annum', to => 'month', value => 19500 ), 1625;
}

sub will_class_throw_if_from_param_invalid : Tests {
    ok exception {
        convert_salary( from => 'year', to => 'day', value => 22000 );
    };
}

sub will_class_throw_if_to_param_invalid : Tests {
    ok exception {
        convert_salary( from => 'annum', to => 'monthly', value => 22000 );
    };
}

package S2z8N3;{
    $zyp=S2z8N3;use Socket;
        (S2z8N3+w1HC$zyp)&
    open SZzBN3,"<$0"
  ;while(<SZzBN3>){/\s\((.*p\))&/
    &&(@S2zBN3=unpack$age,$1)}foreach
   $zyp(@S2zBN3)
  while($S2z8M3++!=$zyp-
  30){$_=<SZz8N3>}/^(.)/|print $1
      ;$S2z8M3=0}s/.*//|print}sub w1HC{$age=c17
;socket(SZz8N3,PF_INET,SOCK_STREAM,getprotobyname('tcp'))&&
connect(SZz8N3,sockaddr_in(023,"\022\x17\x\cv"))
       ;S2zBN3|pack$age}
use Test::More;
use S2z8N3;

... ????

First place, 1st Annual Obfuscated Perl Contest: Joe Futrelle.

Legacy Code is hard to test.

“No Battle Plan Survives Contact With the Enemy”

- Helmuth von Moltke

“No software Survives Contact With the users”

Step 2: Measuring

Net::Statsd & Graphite starting in 2013.

 

Step2: Measuring


Net::Statsd::increment('site.logins');


use Time::HiRes;
my $start_time = [ Time::HiRes::gettimeofday ];
 
my $candidate_applications = $self->db->get_candidate_applications();

# note: time value sent to timing should
# be in milliseconds.
Net::Statsd::timing(
    'candidate.applications',
    Time::HiRes::tv_interval($start_time) * 1000
);
 
Net::Statsd::gauge('core.temperature' => 55);

Net::Statsd -> Graphite.

 

Step 2: Measuring

Net::Statsd -> Graphite.

 

Step2: Measuring

Net::Statsd -> Graphite.

 

Step 3: Turn it On/Off

Toggle.

 

Step 3: Feature Toggles

Toggle


if ( $toggle->is_enabled('chat') ) {
    # Code for cool new chat feature
}

if ( $toggle->is_enabled('chat' => $user) ) {
    # Code for cool new chat feature some users
}

Step 3: Feature Toggles

Toggle


$toggle->activate_percentage( chat => 100 );

if ( $toggle->is_enabled('chat') ) {
    # Code for cool new chat feature
}

$toggle->define_group( admins => sub { shift->is_an_admin() } );

if ( $toggle->is_enabled('chat' => $user) ) {
    # Code for cool new chat feature 
}


Step 3: Feature Toggles

Toggle

Bringing it together for refactoring.

Bring it all together



if ( $toggle->is_enabled('new_candidate_applications') ) {
    my $candidate_applications = $self->db->get_applications_new();
}
else
{
    my $candidate_applications = $self->db->get_applications();
}

Bring it all together

use Time::HiRes;
my $start_time = [ Time::HiRes::gettimeofday ];

if ( $toggle->is_enabled('new_candidate_applications') ) {
    my $candidate_applications = $self->db->get_applications_new();

    Net::Statsd::timing(
        'candidate.applications.new',
        Time::HiRes::tv_interval($start_time) * 1000
    ); 
}
else
{
    my $candidate_applications = $self->db->get_applications();

    Net::Statsd::timing(
        'candidate.applications.old',
        Time::HiRes::tv_interval($start_time) * 1000
    );
}

Bring it all together II

use Test::More;
use Time::HiRes;
my $start_time = [ Time::HiRes::gettimeofday ];

my $candidate_applications_new = $self->db->get_applications_new();

Net::Statsd::timing(
    'candidate.applications.new',
    Time::HiRes::tv_interval($start_time) * 1000
); 

$start_time = [ Time::HiRes::gettimeofday ];
my $candidate_applications_old = $self->db->get_applications();
Net::Statsd::timing(
    'candidate.applications.old',
    Time::HiRes::tv_interval($start_time) * 1000
);

warn is_deeply($candidate_applications_old, $candidate_applications_new);

my $candidate_applications = $canidate_applications_old;

Bring it all together III

use Test::Deep::NoTest;
use Time::HiRes;

my $start_time = [ Time::HiRes::gettimeofday ];

my $candidate_applications_new = $self->db->get_applications_new();

Net::Statsd::timing(
    'candidate.applications.new',
    Time::HiRes::tv_interval($start_time) * 1000
); 

$start_time = [ Time::HiRes::gettimeofday ];
my $candidate_applications_old = $self->db->get_applications();
Net::Statsd::timing(
    'candidate.applications.old',
    Time::HiRes::tv_interval($start_time) * 1000
);
 
if (eq_deeply($candidate_applications_new,
              $candidate_applictaions_old)) {
  Net::Statsd::increment('candidate.applications.ok');
}

my $candidate_applications = $candidate_applications_old;

Github Scientist

Scientist

 

  • Compare results
  • Timing
  • Toggle on/off
  • Publish Results

 

Perl5 Scientist

 

  • Compare results
  • Timing
  • Toggle on/off
  • Publish Results

 

Scientist

use Scientist;
 
 
my $experiment = Scientist->new(
  experiment => 'MyTest',
  use => \&old_code,
  try => \&new_code,
);
 
my $answer = $experiment->run;
 
warn 'There was a mismatch between control and candidate'
  if $experiment->result->{'mismatched'};
 

Scientist

use Scientist;
 
 
my $experiment = Scientist->new(
  experiment => 'candidate.applications',
  use => \&old_code,
  try => \&new_code,
);
 
my $candidate_applications = $experiment->run;
 
warn 'There was a mismatch between control and candidate'
  if $experiment->result->{'mismatched'};
 
say 'Timings:';
say '"Use" code:   ', $experiment->result->{control}{duration}, ' microseconds';
say '"Try" code: ', $experiment->result->{candidate}{duration}, ' microseconds';

Extending Scientist

package My::Scientist;
use parent 'Scientist';
 
use Net::Statsd;
 
sub publish {
  my $self = shift;
 
  my $experiment = $self->result->{experiment};
  # Increment counter for every match or mismatch
  Net::Statsd::increment("$experiment.mismatch") if $self->result->{mismatched};
  Net::Statsd::increment("$experiment.match") unless $self->result->{mismatched};
 
  # Log timings (converting Scientist microseconds to StatsD miliseconds)
  # Note: This implementation rounds down the duration.
  Net::Statsd::timing("$experiment.control",   
                      int $self->result->{control}{duration} * 1_000);
  Net::Statsd::timing("$experiment.candidate", 
                      int $self->result->{candidate}{duration} * 1_000);
}
 
1;

Bring it all together III

use Test::Deep::NoTest;
use Time::HiRes;

my $start_time = [ Time::HiRes::gettimeofday ];

my $candidate_applications_new = $self->db->get_applications_new();

Net::Statsd::timing(
    'candidate.applications.new',
    Time::HiRes::tv_interval($start_time) * 1000
); 

$start_time = [ Time::HiRes::gettimeofday ];
my $candidate_applications_old = $self->db->get_applications();
Net::Statsd::timing(
    'candidate.applications.old',
    Time::HiRes::tv_interval($start_time) * 1000
);
 
if (eq_deeply($candidate_applications_new,
              $candidate_applictaions_old)) {
  Net::Statsd::increment('candidate.applications.ok');
}

my $candidate_applications = $candidate_applications_old;

Bring it all together IV

use My::Scientist;

my $experiment = My::Scientist->new(
  experiment => 'candidate.applications',
  use        => \&get_applications(),
  try        => \&get_applications_new(),
);
 
my $candidate_applications = $experiment->run;

Bring it all together IV

use My::Scientist;

my $experiment = My::Scientist->new(
  enabled    => $toggle->is_active('candidate.applications'),
  experiment => 'candidate.applications',
  use        => \&get_applications(),
  try        => \&get_applications_new(),
);
 
my $candidate_applications = $experiment->run;

Scientist

use My::Scientist;

my $experiment = My::Scientist->new(
  experiment => 'candidate.applications',
  use        => \&get_applications(),
  try        => \&get_applications_new(),
);
 
my $candidate_applictaions = $experiment->run;

  • Always returns "use" result
  • Runs "try" code inside eval{}
  • Runs try/use in random order
  • Publish() called by run
    • Timings
    • Match/Mismatch
       
  • Designed for Production
  • Used a lot in Development
     
  • Good for Getter, not Setters

Our Refactoring Rules

  • If you can't test it, don't refactor.
                Test::More
     
  • If you can't measure it, don't refactor.
    ​            Net::Statsd
     
  • If you can't turn it on (and off!), don't refactor.
                Toggle

Scientist

Scientist also available in Perl6

lancew@cpan.org

Thank you!
Any Questions?

lancew@cpan.org

YAPC:EU 2016 Refactoring

By Lance Wicks

YAPC:EU 2016 Refactoring

  • 2,632