use std::collections::{HashMap, VecDeque};
use super::Job;
use crate::extranonce_manager::{prefix::RetiredExtranoncePrefixes, ExtranoncePrefix};
pub(crate) const MAX_FUTURE_JOBS: usize = 16;
pub(crate) const MAX_PAST_JOBS: usize = 16;
#[derive(Debug)]
pub(crate) struct JobStore<T: Job> {
future_template_to_job_id: HashMap<u64, u32>,
future_template_order: VecDeque<u64>,
future_jobs: HashMap<u32, T>,
active_job: Option<T>,
past_jobs: HashMap<u32, T>,
past_job_order: VecDeque<u32>,
stale_jobs: HashMap<u32, T>,
retired_extranonce_prefixes: RetiredExtranoncePrefixes,
max_past_jobs: usize,
}
impl<T: Job> JobStore<T> {
pub fn new(max_past_jobs: usize) -> Self {
debug_assert!(max_past_jobs > 0, "max_past_jobs must be nonzero");
Self {
future_template_to_job_id: HashMap::new(),
future_template_order: VecDeque::new(),
future_jobs: HashMap::new(),
active_job: None,
past_jobs: HashMap::new(),
past_job_order: VecDeque::new(),
stale_jobs: HashMap::new(),
retired_extranonce_prefixes: RetiredExtranoncePrefixes::default(),
max_past_jobs,
}
}
}
impl<T: Job> Default for JobStore<T> {
fn default() -> Self {
Self::new(MAX_PAST_JOBS)
}
}
impl<T: Job> JobStore<T> {
pub fn add_future_job(&mut self, template_id: u64, new_job: T) -> u32 {
let mut dropped_job = false;
let new_job_id = new_job.get_job_id();
if let Some(old_job_id) = self
.future_template_to_job_id
.insert(template_id, new_job_id)
{
self.future_jobs.remove(&old_job_id);
dropped_job = true;
}
self.future_jobs.insert(new_job_id, new_job);
self.future_template_order.retain(|id| *id != template_id);
self.future_template_order.push_back(template_id);
if self.future_jobs.len() > MAX_FUTURE_JOBS {
if let Some(evicted_template_id) = self.future_template_order.pop_front() {
if let Some(evicted_job_id) =
self.future_template_to_job_id.remove(&evicted_template_id)
{
self.future_jobs.remove(&evicted_job_id);
dropped_job = true;
}
}
}
if dropped_job {
self.prune_retired_extranonce_prefixes();
}
new_job_id
}
fn retire_to_past(&mut self, job: T) -> Option<u32> {
let job_id = job.get_job_id();
self.past_jobs.insert(job_id, job);
self.past_job_order.retain(|id| *id != job_id);
self.past_job_order.push_back(job_id);
if self.past_jobs.len() > self.max_past_jobs {
if let Some(evicted_job_id) = self.past_job_order.pop_front() {
self.past_jobs.remove(&evicted_job_id);
self.prune_retired_extranonce_prefixes();
return Some(evicted_job_id);
}
}
None
}
fn retire_active_to_past_uncapped(&mut self) {
if let Some(active_job) = self.active_job.take() {
let job_id = active_job.get_job_id();
self.past_jobs.insert(job_id, active_job);
self.past_job_order.retain(|id| *id != job_id);
self.past_job_order.push_back(job_id);
}
}
pub fn add_active_job(&mut self, job: T) -> Option<u32> {
let job_id = job.get_job_id();
self.stale_jobs.remove(&job_id);
let evicted_job_id = match self.active_job.replace(job) {
Some(displaced_job) => self.retire_to_past(displaced_job),
None => None,
};
if self.past_jobs.remove(&job_id).is_some() {
self.past_job_order.retain(|id| *id != job_id);
self.prune_retired_extranonce_prefixes();
}
evicted_job_id
}
pub fn replace_active_job(&mut self, job: T) {
self.active_job = Some(job);
}
pub fn activate_future_job_replacing_active(
&mut self,
template_id: u64,
prev_hash_header_timestamp: u32,
) -> bool {
let activated = self.activate_future_job(template_id, prev_hash_header_timestamp);
if activated {
self.stale_jobs.clear();
}
activated
}
pub fn activate_future_job(
&mut self,
template_id: u64,
prev_hash_header_timestamp: u32,
) -> bool {
let mut future_job =
if let Some(job_id) = self.future_template_to_job_id.remove(&template_id) {
if let Some(job) = self.future_jobs.remove(&job_id) {
job
} else {
return false;
}
} else {
return false;
};
self.retire_active_to_past_uncapped();
future_job.activate(prev_hash_header_timestamp);
let activated_job_id = future_job.get_job_id();
self.active_job = Some(future_job);
self.future_jobs.clear();
self.future_template_to_job_id.clear();
self.future_template_order.clear();
self.mark_past_jobs_as_stale();
self.stale_jobs.remove(&activated_job_id);
true
}
pub fn deactivate_job(&mut self) {
self.retire_active_to_past_uncapped();
}
pub fn clear_active_job(&mut self) {
self.active_job = None;
}
pub fn mark_past_jobs_as_stale(&mut self) {
self.stale_jobs = std::mem::take(&mut self.past_jobs);
self.past_job_order.clear();
self.prune_retired_extranonce_prefixes();
}
pub fn retire_extranonce_prefix(&mut self, extranonce_prefix: ExtranoncePrefix) {
self.retired_extranonce_prefixes.retire(
extranonce_prefix,
self.future_jobs
.values()
.chain(self.active_job.iter())
.chain(self.past_jobs.values())
.map(|job| job.get_extranonce_prefix()),
);
}
fn prune_retired_extranonce_prefixes(&mut self) {
self.retired_extranonce_prefixes.prune(
self.future_jobs
.values()
.chain(self.active_job.iter())
.chain(self.past_jobs.values())
.map(|job| job.get_extranonce_prefix()),
);
}
pub fn get_future_job_id_from_template_id(&self, template_id: u64) -> Option<u32> {
self.future_template_to_job_id.get(&template_id).cloned()
}
pub fn get_active_job(&self) -> Option<&T> {
self.active_job.as_ref()
}
pub fn has_future_jobs(&self) -> bool {
!self.future_jobs.is_empty()
}
pub fn get_future_job(&self, job_id: u32) -> Option<&T> {
self.future_jobs.get(&job_id)
}
pub fn get_past_job(&self, job_id: u32) -> Option<&T> {
self.past_jobs.get(&job_id)
}
pub fn get_stale_job(&self, job_id: u32) -> Option<&T> {
self.stale_jobs.get(&job_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extranonce_manager::ExtranonceAllocator;
struct DummyJob {
job_id: u32,
}
impl Job for DummyJob {
fn get_job_id(&self) -> u32 {
self.job_id
}
fn get_extranonce_prefix(&self) -> &[u8] {
&[]
}
fn activate(&mut self, _prev_hash_header_timestamp: u32) {}
}
#[test]
fn future_jobs_are_bounded() {
let mut store = JobStore::new(MAX_PAST_JOBS);
let flood_size = 10_000u64;
for template_id in 0..flood_size {
store.add_future_job(
template_id,
DummyJob {
job_id: template_id as u32,
},
);
}
for template_id in 0..flood_size - MAX_FUTURE_JOBS as u64 {
assert!(store
.get_future_job_id_from_template_id(template_id)
.is_none());
assert!(store.get_future_job(template_id as u32).is_none());
}
for template_id in flood_size - MAX_FUTURE_JOBS as u64..flood_size {
assert!(store
.get_future_job_id_from_template_id(template_id)
.is_some());
assert!(store.get_future_job(template_id as u32).is_some());
}
}
#[test]
fn past_jobs_respect_a_custom_cap() {
let custom_cap = 2;
assert!(custom_cap < MAX_PAST_JOBS);
let mut store = JobStore::new(custom_cap);
for job_id in 0..10u32 {
let evicted_job_id = store.add_active_job(DummyJob { job_id });
if job_id as usize > custom_cap {
assert_eq!(evicted_job_id, Some(job_id - custom_cap as u32 - 1));
} else {
assert_eq!(evicted_job_id, None);
}
}
assert_eq!(store.past_jobs.len(), custom_cap);
assert_eq!(store.past_job_order.len(), custom_cap);
assert!(store.get_past_job(8).is_some());
assert!(store.get_past_job(7).is_some());
for job_id in 0..7u32 {
assert!(store.get_past_job(job_id).is_none());
}
}
#[test]
fn default_job_store_uses_the_default_cap() {
let store: JobStore<DummyJob> = JobStore::default();
assert_eq!(store.max_past_jobs, MAX_PAST_JOBS);
}
#[test]
fn past_jobs_are_bounded() {
let mut store = JobStore::new(MAX_PAST_JOBS);
let flood_size = 10_000u32;
for job_id in 0..flood_size {
let evicted_job_id = store.add_active_job(DummyJob { job_id });
if job_id as usize > MAX_PAST_JOBS {
assert_eq!(evicted_job_id, Some(job_id - MAX_PAST_JOBS as u32 - 1));
} else {
assert_eq!(evicted_job_id, None);
}
}
for job_id in 0..flood_size - 1 - MAX_PAST_JOBS as u32 {
assert!(store.get_past_job(job_id).is_none());
}
for job_id in flood_size - 1 - MAX_PAST_JOBS as u32..flood_size - 1 {
assert!(store.get_past_job(job_id).is_some());
}
assert_eq!(
store.get_active_job().map(|job| job.get_job_id()),
Some(flood_size - 1)
);
}
struct PrefixedJob {
job_id: u32,
prefix: Vec<u8>,
}
impl Job for PrefixedJob {
fn get_job_id(&self) -> u32 {
self.job_id
}
fn get_extranonce_prefix(&self) -> &[u8] {
&self.prefix
}
fn activate(&mut self, _prev_hash_header_timestamp: u32) {}
}
#[test]
fn tip_transition_moves_displaced_and_all_past_jobs_to_stale() {
let mut store = JobStore::new(MAX_PAST_JOBS);
for job_id in 0..=MAX_PAST_JOBS as u32 {
store.add_active_job(DummyJob { job_id });
}
let future_job_id = 100;
store.add_future_job(
1,
DummyJob {
job_id: future_job_id,
},
);
assert!(store.activate_future_job(1, 0));
for job_id in 0..=MAX_PAST_JOBS as u32 {
assert!(store.get_stale_job(job_id).is_some());
}
assert_eq!(store.stale_jobs.len(), MAX_PAST_JOBS + 1);
assert!(store.past_jobs.is_empty());
assert_eq!(
store.get_active_job().map(|job| job.get_job_id()),
Some(future_job_id)
);
}
#[test]
fn activation_keeps_retired_prefix_of_activated_job() {
let mut store = JobStore::new(MAX_PAST_JOBS);
let mut allocator = ExtranonceAllocator::new(vec![], 1, 2).unwrap();
let old_prefix = allocator.allocate_extended(0).unwrap();
store.add_future_job(
1,
PrefixedJob {
job_id: 100,
prefix: old_prefix.as_bytes().to_vec(),
},
);
store.retire_extranonce_prefix(old_prefix.into());
assert_eq!(store.retired_extranonce_prefixes.len(), 1);
for job_id in 0..=MAX_PAST_JOBS as u32 {
store.add_active_job(PrefixedJob {
job_id,
prefix: vec![2u8],
});
}
assert!(store.activate_future_job(1, 0));
assert_eq!(store.retired_extranonce_prefixes.len(), 1);
}
#[test]
fn dropped_jobs_release_retired_extranonce_prefixes() {
let mut store = JobStore::new(MAX_PAST_JOBS);
let mut allocator = ExtranonceAllocator::new(vec![], 1, 2).unwrap();
let old_prefix = allocator.allocate_extended(0).unwrap();
store.add_future_job(
1,
PrefixedJob {
job_id: 1,
prefix: old_prefix.as_bytes().to_vec(),
},
);
store.retire_extranonce_prefix(old_prefix.into());
assert_eq!(store.retired_extranonce_prefixes.len(), 1);
store.add_future_job(
1,
PrefixedJob {
job_id: 2,
prefix: vec![2u8],
},
);
assert!(store.retired_extranonce_prefixes.is_empty());
}
#[test]
fn reused_template_id_evicts_superseded_future_job() {
let mut store = JobStore::new(MAX_PAST_JOBS);
let old_job_id = store.add_future_job(1, DummyJob { job_id: 10 });
let new_job_id = store.add_future_job(1, DummyJob { job_id: 11 });
assert!(store.get_future_job(old_job_id).is_none());
assert!(store.get_future_job(new_job_id).is_some());
assert_eq!(
store.get_future_job_id_from_template_id(1),
Some(new_job_id)
);
}
#[test]
fn dead_allocation_tokens_are_not_retained() {
let prefix = vec![0u8];
let mut store = JobStore::new(MAX_PAST_JOBS);
store.add_active_job(PrefixedJob {
job_id: 1,
prefix: prefix.clone(),
});
for _ in 0..10_000 {
let mut allocator = ExtranonceAllocator::new(vec![], 1, 2).unwrap();
let allocated = allocator.allocate_extended(0).unwrap();
assert_eq!(allocated.as_bytes(), prefix.as_slice());
store.retire_extranonce_prefix(allocated.into());
}
assert_eq!(store.retired_extranonce_prefixes.len(), 1);
let allocated = ExtranonceAllocator::new(vec![], 1, 2)
.unwrap()
.allocate_extended(0)
.unwrap();
store.retire_extranonce_prefix(allocated.into());
assert!(store.retired_extranonce_prefixes.is_empty());
store.retire_extranonce_prefix(ExtranoncePrefix::from_wire(prefix).unwrap());
assert!(store.retired_extranonce_prefixes.is_empty());
}
#[test]
fn a_job_installed_under_a_stale_id_replaces_the_stale_job() {
let mut store = JobStore::new(MAX_PAST_JOBS);
store.add_active_job(DummyJob { job_id: 1 });
store.deactivate_job();
store.mark_past_jobs_as_stale();
assert!(store.get_stale_job(1).is_some());
store.add_active_job(DummyJob { job_id: 1 });
assert_eq!(store.get_active_job().unwrap().get_job_id(), 1);
assert!(store.get_stale_job(1).is_none());
store.add_future_job(7, DummyJob { job_id: 1 });
assert!(store.activate_future_job(7, 0));
assert_eq!(store.get_active_job().unwrap().get_job_id(), 1);
assert!(store.get_stale_job(1).is_none());
assert!(store.stale_jobs.is_empty());
}
#[test]
fn a_job_retiring_under_a_past_id_replaces_the_past_job() {
let mut store = JobStore::new(2);
store.add_active_job(DummyJob { job_id: 1 });
store.add_active_job(DummyJob { job_id: 2 });
store.add_active_job(DummyJob { job_id: 1 });
assert_eq!(store.add_active_job(DummyJob { job_id: 3 }), None);
assert_eq!(store.past_job_order, VecDeque::from(vec![2, 1]));
assert_eq!(store.past_jobs.len(), 2);
assert_eq!(store.add_active_job(DummyJob { job_id: 4 }), Some(2));
assert!(store.get_past_job(1).is_some());
assert!(store.get_past_job(3).is_some());
}
#[test]
fn install_keeps_retired_prefix_of_the_installed_job() {
let mut store = JobStore::new(1);
let mut allocator = ExtranonceAllocator::new(vec![], 1, 2).unwrap();
let old_prefix = allocator.allocate_extended(0).unwrap();
let old_prefix_bytes = old_prefix.as_bytes().to_vec();
store.add_active_job(PrefixedJob {
job_id: 1,
prefix: old_prefix_bytes.clone(),
});
store.retire_extranonce_prefix(old_prefix.into());
store.add_active_job(PrefixedJob {
job_id: 2,
prefix: vec![2u8],
});
assert_eq!(store.retired_extranonce_prefixes.len(), 1);
assert_eq!(
store.add_active_job(PrefixedJob {
job_id: 3,
prefix: old_prefix_bytes,
}),
Some(1)
);
assert_eq!(store.retired_extranonce_prefixes.len(), 1);
}
#[test]
fn a_job_installed_under_a_past_id_replaces_the_past_job() {
let mut store = JobStore::new(3);
store.add_active_job(DummyJob { job_id: 2 });
store.add_active_job(DummyJob { job_id: 1 });
store.add_active_job(DummyJob { job_id: 3 });
assert!(store.get_past_job(1).is_some());
assert_eq!(store.add_active_job(DummyJob { job_id: 1 }), None);
assert!(store.get_past_job(1).is_none());
assert_eq!(store.get_active_job().unwrap().get_job_id(), 1);
assert_eq!(store.past_job_order, VecDeque::from(vec![2, 3]));
assert_eq!(store.past_jobs.len(), 2);
}
}