-
Notifications
You must be signed in to change notification settings - Fork 1
feat: cache default equity projection for leaderboard #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
2fa760f
1506179
bdb57d3
57501a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package Bracket::Schema::Result::EquityCache; | ||
|
|
||
| use strict; | ||
| use warnings; | ||
|
|
||
| use base 'DBIx::Class::Core'; | ||
|
|
||
| =head1 NAME | ||
|
|
||
| Bracket::Schema::Result::EquityCache - Per-player default equity projection cache | ||
|
|
||
| =cut | ||
|
|
||
| __PACKAGE__->table("equity_cache"); | ||
|
|
||
| =head1 ACCESSORS | ||
|
|
||
| =head2 player_id | ||
|
|
||
| data_type: INT | ||
| is_nullable: 0 | ||
|
|
||
| =head2 cache_key | ||
|
|
||
| data_type: VARCHAR | ||
| size: 32 | ||
| is_nullable: 0 | ||
| default: 'default' | ||
|
|
||
| =head2 current_points | ||
|
|
||
| data_type: INT | ||
| is_nullable: 0 | ||
| default: 0 | ||
|
|
||
| =head2 max_possible_points | ||
|
|
||
| data_type: INT | ||
| is_nullable: 0 | ||
| default: 0 | ||
|
|
||
| =head2 projected_first_pct | ||
|
|
||
| data_type: VARCHAR | ||
| size: 16 | ||
| is_nullable: 0 | ||
| default: '0.00' | ||
|
|
||
| =head2 projected_podium_pct | ||
|
|
||
| data_type: VARCHAR | ||
| size: 16 | ||
| is_nullable: 0 | ||
| default: '0.00' | ||
|
|
||
| =head2 projected_score_avg | ||
|
|
||
| data_type: VARCHAR | ||
| size: 16 | ||
| is_nullable: 0 | ||
| default: '0.00' | ||
|
|
||
| =cut | ||
|
|
||
| __PACKAGE__->add_columns( | ||
| "player_id", | ||
| { data_type => "INT", is_nullable => 0 }, | ||
| "cache_key", | ||
| { data_type => "VARCHAR", size => 32, is_nullable => 0, default_value => 'default' }, | ||
| "current_points", | ||
| { data_type => "INT", is_nullable => 0, default_value => 0 }, | ||
| "max_possible_points", | ||
| { data_type => "INT", is_nullable => 0, default_value => 0 }, | ||
| "projected_first_pct", | ||
| { data_type => "VARCHAR", size => 16, is_nullable => 0, default_value => '0.00' }, | ||
| "projected_podium_pct", | ||
| { data_type => "VARCHAR", size => 16, is_nullable => 0, default_value => '0.00' }, | ||
| "projected_score_avg", | ||
| { data_type => "VARCHAR", size => 16, is_nullable => 0, default_value => '0.00' }, | ||
| ); | ||
|
|
||
| __PACKAGE__->set_primary_key("player_id", "cache_key"); | ||
|
|
||
| 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -510,4 +510,130 @@ sub _dedupe { | |
| return grep { defined $_ && !$seen{$_}++ } @values; | ||
| } | ||
|
|
||
| use constant DEFAULT_CACHE_KEY => 'default'; | ||
| use constant DEFAULT_ITERATIONS => 2000; | ||
| use constant DEFAULT_SEED => 17; | ||
|
|
||
| =head2 refresh_default_cache | ||
|
|
||
| Bracket::Service::EquityProjection->refresh_default_cache($schema); | ||
|
|
||
| Runs the default equity projection (iterations=2000, seed=17) and stores the | ||
| per-player results in the C<equity_cache> table under cache_key C<'default'>. | ||
| Any previous rows for that cache_key are replaced atomically. | ||
|
|
||
| =cut | ||
|
|
||
| sub refresh_default_cache { | ||
| my ($class, $schema) = @_; | ||
| return if !$schema; | ||
|
|
||
| _ensure_cache_table($schema); | ||
|
|
||
| my $projection = $class->project($schema, { | ||
| iterations => DEFAULT_ITERATIONS, | ||
| seed => DEFAULT_SEED, | ||
| }); | ||
|
|
||
| $schema->txn_do(sub { | ||
| $schema->resultset('EquityCache')->search({ cache_key => DEFAULT_CACHE_KEY })->delete; | ||
| for my $row (@{$projection->{player_projections} || []}) { | ||
| next if !$row->{player_id}; | ||
| $schema->resultset('EquityCache')->create({ | ||
| player_id => $row->{player_id}, | ||
| cache_key => DEFAULT_CACHE_KEY, | ||
| current_points => $row->{current_points} // 0, | ||
| max_possible_points => $row->{max_possible_points} // 0, | ||
| projected_first_pct => $row->{projected_first_pct} // '0.00', | ||
| projected_podium_pct => $row->{projected_podium_pct} // '0.00', | ||
| projected_score_avg => $row->{projected_score_avg} // '0.00', | ||
| }); | ||
| } | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| =head2 load_default_cache | ||
|
|
||
| my $projection = Bracket::Service::EquityProjection->load_default_cache($schema); | ||
|
|
||
| Loads the cached default equity projection from the database. Returns a | ||
| hashref with a C<player_projections> key in the same format as C<project()>, | ||
| or C<undef> if the cache is empty. | ||
|
|
||
| =cut | ||
|
|
||
| sub load_default_cache { | ||
| my ($class, $schema) = @_; | ||
| return undef if !$schema; | ||
|
|
||
| _ensure_cache_table($schema); | ||
|
|
||
| my @rows = $schema->resultset('EquityCache') | ||
| ->search({ cache_key => DEFAULT_CACHE_KEY }) | ||
| ->all; | ||
| return undef if !@rows; | ||
|
|
||
| my @player_projections = map { | ||
| { | ||
| player_id => $_->get_column('player_id'), | ||
| current_points => $_->get_column('current_points'), | ||
| max_possible_points => $_->get_column('max_possible_points'), | ||
| projected_first_pct => $_->get_column('projected_first_pct'), | ||
| projected_podium_pct => $_->get_column('projected_podium_pct'), | ||
| projected_score_avg => $_->get_column('projected_score_avg'), | ||
| } | ||
| } @rows; | ||
|
|
||
| return { player_projections => \@player_projections }; | ||
| } | ||
|
|
||
| sub _ensure_cache_table { | ||
| my ($schema) = @_; | ||
| return if !$schema; | ||
|
|
||
| my $storage = $schema->storage; | ||
| my $dbh = $storage->dbh; | ||
| my $driver = lc($dbh->{Driver}{Name} || ''); | ||
|
|
||
| if ($driver eq 'mysql') { | ||
| $dbh->do(q{ | ||
| create table if not exists equity_cache ( | ||
| player_id int not null, | ||
| cache_key varchar(32) not null default 'default', | ||
| current_points int not null default 0, | ||
| max_possible_points int not null default 0, | ||
| projected_first_pct varchar(16) not null default '0.00', | ||
| projected_podium_pct varchar(16) not null default '0.00', | ||
| projected_score_avg varchar(16) not null default '0.00', | ||
| primary key (player_id, cache_key) | ||
| ) | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| if ($driver eq 'sqlite') { | ||
| $dbh->do(q{ | ||
| create table if not exists equity_cache ( | ||
| player_id integer not null, | ||
| cache_key varchar(32) not null default 'default', | ||
| current_points integer not null default 0, | ||
| max_possible_points integer not null default 0, | ||
| projected_first_pct varchar(16) not null default '0.00', | ||
| projected_podium_pct varchar(16) not null default '0.00', | ||
| projected_score_avg varchar(16) not null default '0.00', | ||
| primary key (player_id, cache_key) | ||
| ) | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| my $table = eval { $schema->storage->dbh->selectrow_arrayref(q{ | ||
| select 1 from equity_cache where 1 = 0 | ||
| }) }; | ||
| return if $table || !$@; | ||
|
|
||
| die 'equity_cache table is missing and automatic creation is only implemented for MySQL/SQLite'; | ||
| } | ||
|
||
|
|
||
| 1; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_update_points_for_schema now always runs refresh_default_cache, and any exception from cache table creation or the projection itself will abort update_points entirely. Since update_points is an admin-critical scoring path, consider wrapping the cache refresh in eval/try so point updates still succeed (and report cache refresh failure separately) rather than failing the whole operation.