refine timestamps in json signals api

*   API change: in update signals, allow setting a start time relative
    to now. This is an accuracy improvement in the case where the client
    has been retrying an initial request for a while. Kind of an obscure
    corner case but easy enough to address. And use a more convenient
    enum representation.

*   in update signals, choose `now` before acquiring the database lock.
    If lock acquisition takes a long time, this more accurately reflects
    the time the caller intended.

*   in general, make Time and Duration (de)serializable and use them
    in json types. This makes the types more self-describing, with
    better debug printing on both the server side and on the client
    library (in moonfire-playground). To make this work, base has to
    import serde which initially seemed like poor layering to me, but
    serde seems to be imported in some pretty foundational Rust crates
    for this reason. I'll go with it.
This commit is contained in:
Scott Lamb
2021-04-21 10:44:01 -07:00
parent 5da5494dfb
commit 1e314e09d0
6 changed files with 61 additions and 53 deletions

View File

@@ -2,6 +2,7 @@
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
use base::time::{Duration, Time};
use db::auth::SessionHash;
use failure::{format_err, Error};
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
@@ -77,9 +78,9 @@ pub struct CameraConfig<'a> {
#[serde(rename_all = "camelCase")]
pub struct Stream<'a> {
pub retain_bytes: i64,
pub min_start_time_90k: Option<i64>,
pub max_end_time_90k: Option<i64>,
pub total_duration_90k: i64,
pub min_start_time_90k: Option<Time>,
pub max_end_time_90k: Option<Time>,
pub total_duration_90k: Duration,
pub total_sample_file_bytes: i64,
pub fs_bytes: i64,
@@ -113,10 +114,10 @@ pub struct Signal<'a> {
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PostSignalsEndBase {
Epoch,
Now,
#[serde(tag = "base", content = "rel90k", rename_all = "camelCase")]
pub enum PostSignalsTimeBase {
Epoch(Time),
Now(Duration),
}
#[derive(Deserialize)]
@@ -137,21 +138,20 @@ pub struct LogoutRequest<'a> {
pub struct PostSignalsRequest {
pub signal_ids: Vec<u32>,
pub states: Vec<u16>,
pub start_time_90k: Option<i64>,
pub end_base: PostSignalsEndBase,
pub rel_end_time_90k: Option<i64>,
pub start: PostSignalsTimeBase,
pub end: PostSignalsTimeBase,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PostSignalsResponse {
pub time_90k: i64,
pub time_90k: Time,
}
#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Signals {
pub times_90k: Vec<i64>,
pub times_90k: Vec<Time>,
pub signal_ids: Vec<u32>,
pub states: Vec<u16>,
}
@@ -238,9 +238,9 @@ impl<'a> Stream<'a> {
.ok_or_else(|| format_err!("missing stream {}", id))?;
Ok(Some(Stream {
retain_bytes: s.retain_bytes,
min_start_time_90k: s.range.as_ref().map(|r| r.start.0),
max_end_time_90k: s.range.as_ref().map(|r| r.end.0),
total_duration_90k: s.duration.0,
min_start_time_90k: s.range.as_ref().map(|r| r.start),
max_end_time_90k: s.range.as_ref().map(|r| r.end),
total_duration_90k: s.duration,
total_sample_file_bytes: s.sample_file_bytes,
fs_bytes: s.fs_bytes,
days: if include_days { Some(s.days()) } else { None },
@@ -269,9 +269,9 @@ impl<'a> Stream<'a> {
map.serialize_key(k.as_ref())?;
let bounds = k.bounds();
map.serialize_value(&StreamDayValue {
start_time_90k: bounds.start.0,
end_time_90k: bounds.end.0,
total_duration_90k: v.duration.0,
start_time_90k: bounds.start,
end_time_90k: bounds.end,
total_duration_90k: v.duration,
})?;
}
map.end()
@@ -328,8 +328,8 @@ impl<'a> Signal<'a> {
map.serialize_key(k.as_ref())?;
let bounds = k.bounds();
map.serialize_value(&SignalDayValue {
start_time_90k: bounds.start.0,
end_time_90k: bounds.end.0,
start_time_90k: bounds.start,
end_time_90k: bounds.end,
states: &v.states[..],
})?;
}
@@ -371,16 +371,16 @@ impl<'a> SignalTypeState<'a> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct StreamDayValue {
pub start_time_90k: i64,
pub end_time_90k: i64,
pub total_duration_90k: i64,
pub start_time_90k: Time,
pub end_time_90k: Time,
pub total_duration_90k: Duration,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SignalDayValue<'a> {
pub start_time_90k: i64,
pub end_time_90k: i64,
pub start_time_90k: Time,
pub end_time_90k: Time,
pub states: &'a [u64],
}

View File

@@ -1182,22 +1182,19 @@ impl Service {
let r = extract_json_body(&mut req).await?;
let r: json::PostSignalsRequest =
serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
let mut l = self.db.lock();
let now = recording::Time::new(self.db.clocks().realtime());
let start = r.start_time_90k.map(recording::Time).unwrap_or(now);
let end = match r.end_base {
json::PostSignalsEndBase::Epoch => {
recording::Time(r.rel_end_time_90k.ok_or_else(|| {
bad_req("must specify rel_end_time_90k when end_base is epoch")
})?)
}
json::PostSignalsEndBase::Now => {
now + recording::Duration(r.rel_end_time_90k.unwrap_or(0))
}
let mut l = self.db.lock();
let start = match r.start {
json::PostSignalsTimeBase::Epoch(t) => t,
json::PostSignalsTimeBase::Now(d) => now + d,
};
let end = match r.end {
json::PostSignalsTimeBase::Epoch(t) => t,
json::PostSignalsTimeBase::Now(d) => now + d,
};
l.update_signals(start..end, &r.signal_ids, &r.states)
.map_err(from_base_error)?;
serve_json(&req, &json::PostSignalsResponse { time_90k: now.0 })
serve_json(&req, &json::PostSignalsResponse { time_90k: now })
}
fn get_signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
@@ -1223,7 +1220,7 @@ impl Service {
self.db
.lock()
.list_changes_by_time(time, &mut |c: &db::signal::ListStateChangesRow| {
signals.times_90k.push(c.when.0);
signals.times_90k.push(c.when);
signals.signal_ids.push(c.signal);
signals.states.push(c.state);
});