Centralize API for working set to a single struct

Rather than allow addressing tasks either by working set ID or uuid,
with attendant performance issues, this moves the API for the working
set to a single struct that just serves as a 1-1 mapping of indexes to
UUIDs.  It's up to the caller to use this information.
This commit is contained in:
Dustin J. Mitchell 2021-01-09 22:57:33 +00:00
parent d06f2e5aeb
commit 087769146e
7 changed files with 194 additions and 116 deletions

View file

@ -32,10 +32,12 @@ mod task;
mod taskdb;
pub mod taskstorage;
mod utils;
mod workingset;
pub use config::{ReplicaConfig, ServerConfig};
pub use replica::Replica;
pub use task::{Priority, Status, Tag, Task, TaskMut};
pub use workingset::WorkingSet;
/// Re-exported type from the `uuid` crate, for ease of compatibility for consumers of this crate.
pub use uuid::Uuid;

View file

@ -4,6 +4,7 @@ use crate::server::Server;
use crate::task::{Status, Task};
use crate::taskdb::TaskDB;
use crate::taskstorage::{KVStorage, Operation, TaskMap, TaskStorage};
use crate::workingset::WorkingSet;
use chrono::Utc;
use failure::Fallible;
use log::trace;
@ -87,21 +88,10 @@ impl Replica {
self.taskdb.all_task_uuids()
}
/// Get the "working set" for this replica -- the set of pending tasks, as indexed by small
/// integers
pub fn working_set(&mut self) -> Fallible<Vec<Option<Task>>> {
let working_set = self.taskdb.working_set()?;
let mut res = Vec::with_capacity(working_set.len());
for item in working_set.iter() {
res.push(match item {
Some(u) => match self.taskdb.get_task(*u)? {
Some(tm) => Some(Task::new(*u, tm)),
None => None,
},
None => None,
})
}
Ok(res)
/// Get the "working set" for this replica. This is a snapshot of the current state,
/// and it is up to the caller to decide how long to store this value.
pub fn working_set(&mut self) -> Fallible<WorkingSet> {
Ok(WorkingSet::new(self.taskdb.working_set()?))
}
/// Get an existing task by its UUID
@ -112,33 +102,6 @@ impl Replica {
.map(move |tm| Task::new(uuid, tm)))
}
/// Get an existing task by its working set index
pub fn get_working_set_task(&mut self, i: usize) -> Fallible<Option<Task>> {
let working_set = self.taskdb.working_set()?;
if (i as usize) < working_set.len() {
if let Some(uuid) = working_set[i as usize] {
return Ok(self
.taskdb
.get_task(uuid)?
.map(move |tm| Task::new(uuid, tm)));
}
}
Ok(None)
}
/// Get the working set index for the given task uuid
pub fn get_working_set_index(&mut self, uuid: Uuid) -> Fallible<Option<usize>> {
let working_set = self.taskdb.working_set()?;
for (i, u) in working_set.iter().enumerate() {
if let Some(u) = u {
if *u == uuid {
return Ok(Some(i));
}
}
}
Ok(None)
}
/// Create a new task. The task must not already exist.
pub fn new_task(&mut self, status: Status, description: String) -> Fallible<Task> {
let uuid = Uuid::new_v4();
@ -258,7 +221,8 @@ mod tests {
rep.rebuild_working_set(true).unwrap();
assert!(rep.get_working_set_index(t.get_uuid()).unwrap().is_none());
let ws = rep.working_set().unwrap();
assert!(ws.by_uuid(t.get_uuid()).is_none());
}
#[test]
@ -270,16 +234,13 @@ mod tests {
.unwrap();
let uuid = t.get_uuid();
let t = rep.get_working_set_task(1).unwrap().unwrap();
assert_eq!(t.get_status(), Status::Pending);
assert_eq!(t.get_description(), "to-be-pending");
let ws = rep.working_set().unwrap();
assert_eq!(ws.len(), 1); // only one non-none value
assert!(ws.by_index(0).is_none());
assert_eq!(ws.by_index(1), Some(uuid));
let ws = rep.working_set().unwrap();
assert_eq!(ws.len(), 2);
assert!(ws[0].is_none());
assert_eq!(ws[1].as_ref().unwrap().get_uuid(), uuid);
assert_eq!(rep.get_working_set_index(t.get_uuid()).unwrap().unwrap(), 1);
assert_eq!(ws.by_uuid(t.get_uuid()), Some(1));
}
#[test]

View file

@ -0,0 +1,132 @@
use std::collections::HashMap;
use uuid::Uuid;
/// A WorkingSet represents a snapshot of the working set from a replica.
///
/// A replica's working set is a mapping from small integers to task uuids for all pending tasks.
/// The small integers are meant to be stable, easily-typed identifiers for users to interact with
/// important tasks.
///
/// IMPORTANT: the content of the working set may change at any time that a DB transaction is not
/// in progress, and the data in this type will not be updated automatically. It is up to the
/// caller to decide how long to keep this value, and how much to trust the accuracy of its
/// contents. In practice, the answers are usually "a few milliseconds" and treating unexpected
/// results as non-fatal.
pub struct WorkingSet {
by_index: Vec<Option<Uuid>>,
by_uuid: HashMap<Uuid, usize>,
}
impl WorkingSet {
/// Create a new WorkingSet. Typically this is acquired via `replica.working_set()`
pub(crate) fn new(by_index: Vec<Option<Uuid>>) -> Self {
let mut by_uuid = HashMap::new();
for (index, uuid) in by_index.iter().enumerate() {
if let Some(uuid) = uuid {
by_uuid.insert(*uuid, index);
}
}
Self { by_index, by_uuid }
}
/// Get the "length" of the working set: the total number of uuids in the set.
pub fn len(&self) -> usize {
self.by_index.iter().filter(|e| e.is_some()).count()
}
/// True if the length is zero
pub fn is_empty(&self) -> bool {
self.by_index.iter().all(|e| e.is_none())
}
/// Get the uuid with the given index, if any exists.
pub fn by_index(&self, index: usize) -> Option<Uuid> {
if let Some(Some(uuid)) = self.by_index.get(index) {
Some(*uuid)
} else {
None
}
}
/// Get the index for the given uuid, if any
pub fn by_uuid(&self, uuid: Uuid) -> Option<usize> {
self.by_uuid.get(&uuid).copied()
}
/// Iterate over pairs (index, uuid), in order by index.
pub fn iter(&self) -> impl Iterator<Item = (usize, Uuid)> + '_ {
self.by_index
.iter()
.enumerate()
.filter_map(|(index, uuid)| {
if let Some(uuid) = uuid {
Some((index, *uuid))
} else {
None
}
})
}
}
#[cfg(test)]
mod test {
use super::*;
fn make() -> (Uuid, Uuid, WorkingSet) {
let uuid1 = Uuid::new_v4();
let uuid2 = Uuid::new_v4();
(
uuid1,
uuid2,
WorkingSet::new(vec![None, Some(uuid1), None, Some(uuid2), None]),
)
}
#[test]
fn test_new() {
let (_, uuid2, ws) = make();
assert_eq!(ws.by_index[3], Some(uuid2));
assert_eq!(ws.by_uuid.get(&uuid2), Some(&3));
}
#[test]
fn test_len_and_is_empty() {
let (_, _, ws) = make();
assert_eq!(ws.len(), 2);
assert_eq!(ws.is_empty(), false);
let ws = WorkingSet::new(vec![]);
assert_eq!(ws.len(), 0);
assert_eq!(ws.is_empty(), true);
let ws = WorkingSet::new(vec![None, None, None]);
assert_eq!(ws.len(), 0);
assert_eq!(ws.is_empty(), true);
}
#[test]
fn test_by_index() {
let (uuid1, uuid2, ws) = make();
assert_eq!(ws.by_index(0), None);
assert_eq!(ws.by_index(1), Some(uuid1));
assert_eq!(ws.by_index(2), None);
assert_eq!(ws.by_index(3), Some(uuid2));
assert_eq!(ws.by_index(4), None);
assert_eq!(ws.by_index(100), None); // past the end of the vector
}
#[test]
fn test_by_uuid() {
let (uuid1, uuid2, ws) = make();
let nosuch = Uuid::new_v4();
assert_eq!(ws.by_uuid(uuid1), Some(1));
assert_eq!(ws.by_uuid(uuid2), Some(3));
assert_eq!(ws.by_uuid(nosuch), None);
}
#[test]
fn test_iter() {
let (uuid1, uuid2, ws) = make();
assert_eq!(ws.iter().collect::<Vec<_>>(), vec![(1, uuid1), (3, uuid2),]);
}
}