TODO: Insert QOS Ping ID into dispatchNetworkUp/Down()

pull/1418/head
Rick Carlino 2019-09-05 16:44:12 -05:00
parent 9599f14a98
commit 9858fa343b
18 changed files with 152 additions and 83 deletions

View File

@ -31,8 +31,6 @@ import { dispatchNetworkUp, dispatchNetworkDown } from "../connectivity";
import { Session } from "../session";
import { error } from "../toast/toast";
const A_STRING = expect.any(String);
interface FakeProps {
uuid: string;
method: Method;
@ -67,7 +65,7 @@ describe("responseRejected", () => {
it("undefined error", async () => {
await expect(responseRejected(undefined)).rejects.toEqual(undefined);
expect(dispatchNetworkUp).not.toHaveBeenCalled();
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.api", undefined, A_STRING);
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.api");
});
it("safe error", async () => {
@ -77,7 +75,7 @@ describe("responseRejected", () => {
};
await expect(responseRejected(safeError)).rejects.toEqual(safeError);
expect(dispatchNetworkDown).not.toHaveBeenCalled();
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.api", undefined, A_STRING);
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.api");
});
it("handles 500", async () => {

View File

@ -42,7 +42,6 @@ import { MessageType } from "../../../sequences/interfaces";
import { FbjsEventName } from "farmbot/dist/constants";
import { info, error, success, warning, fun, busy } from "../../../toast/toast";
const A_STRING = expect.any(String);
describe("readStatus()", () => {
it("forces a read_status request to FarmBot", () => {
readStatus();
@ -160,9 +159,9 @@ describe("initLog", () => {
describe("bothUp()", () => {
it("marks MQTT and API as up", () => {
bothUp("tests");
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", undefined, A_STRING);
expect(dispatchNetworkUp).toHaveBeenCalledWith("bot.mqtt", undefined, A_STRING);
bothUp();
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt");
expect(dispatchNetworkUp).toHaveBeenCalledWith("bot.mqtt");
});
});
@ -170,7 +169,7 @@ describe("onOffline", () => {
it("tells the app MQTT is down", () => {
jest.resetAllMocks();
onOffline();
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.mqtt", undefined, A_STRING);
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.mqtt");
expect(error).toHaveBeenCalledWith(Content.MQTT_DISCONNECTED);
});
});
@ -179,7 +178,7 @@ describe("onOnline", () => {
it("tells the app MQTT is up", () => {
jest.resetAllMocks();
onOnline();
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", undefined, A_STRING);
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt");
});
});
@ -205,13 +204,13 @@ describe("onSent", () => {
it("marks MQTT as up", () => {
jest.resetAllMocks();
onSent({ connected: true })();
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", undefined, A_STRING);
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt");
});
it("marks MQTT as down", () => {
jest.resetAllMocks();
onSent({ connected: false })();
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.mqtt", undefined, A_STRING);
expect(dispatchNetworkDown).toHaveBeenCalledWith("user.mqtt");
});
});
@ -235,7 +234,7 @@ describe("onLogs", () => {
log.message = "bot xyz is offline";
fn(log);
globalQueue.maybeWork();
expect(dispatchNetworkDown).toHaveBeenCalledWith("bot.mqtt", undefined, A_STRING);
expect(dispatchNetworkDown).toHaveBeenCalledWith("bot.mqtt");
});
it("handles log fields correctly", () => {

View File

@ -14,29 +14,29 @@ const SHORT_TIME_LATER = new Date(NOW.getTime() + 500).getTime();
const LONGER_TIME_LATER = new Date(NOW.getTime() + 5000).getTime();
describe("dispatchNetworkUp", () => {
const NOW_UP = networkUp("bot.mqtt", NOW.getTime(), "tests");
const LATER_UP = networkUp("bot.mqtt", LONGER_TIME_LATER, "tests");
const NOW_UP = networkUp("bot.mqtt", NOW.getTime());
const LATER_UP = networkUp("bot.mqtt", LONGER_TIME_LATER);
it("calls redux directly", () => {
dispatchNetworkUp("bot.mqtt", NOW.getTime(), "tests");
dispatchNetworkUp("bot.mqtt", NOW.getTime());
expect(mockRedux.store.dispatch).toHaveBeenLastCalledWith(NOW_UP);
dispatchNetworkUp("bot.mqtt", SHORT_TIME_LATER, "tests");
dispatchNetworkUp("bot.mqtt", SHORT_TIME_LATER);
expect(mockRedux.store.dispatch).toHaveBeenLastCalledWith(NOW_UP);
dispatchNetworkUp("bot.mqtt", LONGER_TIME_LATER, "tests");
dispatchNetworkUp("bot.mqtt", LONGER_TIME_LATER);
expect(mockRedux.store.dispatch).toHaveBeenLastCalledWith(LATER_UP);
});
});
describe("dispatchNetworkDown", () => {
const NOW_DOWN = networkDown("user.api", NOW.getTime(), "tests");
const LATER_DOWN = networkDown("user.api", LONGER_TIME_LATER, "tests");
const NOW_DOWN = networkDown("user.api", NOW.getTime());
const LATER_DOWN = networkDown("user.api", LONGER_TIME_LATER);
it("calls redux directly", () => {
dispatchNetworkDown("user.api", NOW.getTime(), "tests");
dispatchNetworkDown("user.api", NOW.getTime());
expect(mockRedux.store.dispatch).toHaveBeenLastCalledWith(NOW_DOWN);
dispatchNetworkDown("user.api", SHORT_TIME_LATER, "tests");
dispatchNetworkDown("user.api", SHORT_TIME_LATER);
expect(mockRedux.store.dispatch).toHaveBeenLastCalledWith(NOW_DOWN);
dispatchNetworkDown("user.api", LONGER_TIME_LATER, "tests");
dispatchNetworkDown("user.api", LONGER_TIME_LATER);
expect(mockRedux.store.dispatch).toHaveBeenLastCalledWith(LATER_DOWN);
});
});

View File

@ -1,6 +1,7 @@
jest.mock("../index", () => ({
dispatchNetworkDown: jest.fn(),
dispatchNetworkUp: jest.fn()
dispatchNetworkUp: jest.fn(),
dispatchQosStart: jest.fn()
}));
const mockTimestamp = 0;
@ -49,14 +50,14 @@ function fakeBot(): Farmbot {
function expectStale() {
expect(dispatchNetworkDown)
.toHaveBeenCalledWith("bot.mqtt", undefined, expect.any(String));
.toHaveBeenCalledWith("bot.mqtt");
}
function expectActive() {
expect(dispatchNetworkUp)
.toHaveBeenCalledWith("bot.mqtt", undefined, expect.any(String));
.toHaveBeenCalledWith("bot.mqtt");
expect(dispatchNetworkUp)
.toHaveBeenCalledWith("user.mqtt", undefined, expect.any(String));
.toHaveBeenCalledWith("user.mqtt");
}
describe("ping util", () => {
@ -67,12 +68,12 @@ describe("ping util", () => {
});
it("marks the bot's connection to MQTT as 'stale'", () => {
markStale();
markStale("TESTS");
expectStale();
});
it("marks the bot's connection to MQTT as 'active'", () => {
markActive();
markActive("TESTS");
expectActive();
});

View File

@ -4,7 +4,7 @@ import { networkUp, networkDown } from "../actions";
describe("connectivityReducer", () => {
it("goes up", () => {
const state = connectivityReducer(DEFAULT_STATE,
networkUp("user.mqtt", undefined, "tests"));
networkUp("user.mqtt"));
expect(state).toBeDefined();
const x = state && state.uptime["user.mqtt"];
if (x) {
@ -17,7 +17,7 @@ describe("connectivityReducer", () => {
it("goes down", () => {
const state = connectivityReducer(DEFAULT_STATE,
networkDown("user.api", undefined, "tests"));
networkDown("user.api"));
const x = state && state.uptime["user.api"];
if (x) {
expect(x.state).toBe("down");

View File

@ -5,10 +5,10 @@ import { ReduxAction } from "../redux/interfaces";
type NetChange = ReduxAction<EdgeStatus>;
const change = (state: "up" | "down") =>
(name: Edge, at = (new Date()).getTime(), why: string): NetChange => {
(name: Edge, at = (new Date()).getTime(), qosPingId?: string): NetChange => {
return {
type: Actions.NETWORK_EDGE_CHANGE,
payload: { name, status: { state, at }, why }
payload: { name, status: { state, at }, qosPingId }
};
};

View File

@ -28,7 +28,7 @@ export class BatchQueue {
store.dispatch(batchInitResources(this.queue));
}
this.clear();
bothUp("Inside BatchQueue instance");
bothUp();
}
push = (resource: TaggedLog) => {

View File

@ -95,9 +95,9 @@ export const batchInitResources =
return { type: Actions.BATCH_INIT, payload };
};
export const bothUp = (why: string) => {
dispatchNetworkUp("user.mqtt", undefined, why);
dispatchNetworkUp("bot.mqtt", undefined, why);
export const bothUp = () => {
dispatchNetworkUp("user.mqtt");
dispatchNetworkUp("bot.mqtt");
};
export function readStatus() {
@ -108,7 +108,7 @@ export function readStatus() {
}
export const onOffline = () => {
dispatchNetworkDown("user.mqtt", undefined, "onOffline() callback");
dispatchNetworkDown("user.mqtt");
error(t(Content.MQTT_DISCONNECTED));
};
@ -117,7 +117,7 @@ export const changeLastClientConnected = (bot: Farmbot) => () => {
"LAST_CLIENT_CONNECTED": JSON.stringify(new Date())
}).catch(noop); // This is internal stuff, don't alert user.
};
const setBothUp = () => bothUp("Got a status message");
const setBothUp = () => bothUp();
const legacyChecks = (getState: GetState) => {
const { controller_version } = getState().bot.hardware.informational_settings;
@ -150,13 +150,12 @@ type Client = { connected?: boolean };
export const onSent = (client: Client) => () => {
const connected = !!client.connected;
const why = `Outbound mqtt.js. client.connected = ${connected}`;
const cb = connected ? dispatchNetworkUp : dispatchNetworkDown;
cb("user.mqtt", undefined, why);
cb("user.mqtt");
};
export function onMalformed() {
bothUp("Got a malformed message");
bothUp();
if (!HACKY_FLAGS.alreadyToldUserAboutMalformedMsg) {
warning(t(Content.MALFORMED_MESSAGE_REC_UPGRADE));
HACKY_FLAGS.alreadyToldUserAboutMalformedMsg = true;
@ -166,11 +165,11 @@ export function onMalformed() {
export const onOnline =
() => {
success(t("Reconnected to the message broker."), t("Online"));
dispatchNetworkUp("user.mqtt", undefined, "MQTT.js is online");
dispatchNetworkUp("user.mqtt");
};
export const onReconnect =
() => warning(t("Attempting to reconnect to the message broker"),
t("Offline"), "yellow");
t("Offline"), "yellow");
export function onPublicBroadcast(payl: unknown) {
console.log(FbjsEventName.publicBroadcast, payl);

View File

@ -1,6 +1,7 @@
import { store } from "../redux/store";
import { networkUp, networkDown } from "./actions";
import { Edge } from "./interfaces";
import { Actions } from "../constants";
/* ABOUT THIS FILE: These functions allow us to mark the network as up or
down from anywhere within the app (even outside of React-Redux). I usually avoid
@ -24,14 +25,23 @@ function bumpThrottle(edge: Edge, now: number) {
lastCalledAt[edge] = now;
}
export let dispatchNetworkUp = (edge: Edge, at = (new Date()).getTime(), why: string) => {
export const dispatchQosStart = (id: string) => {
store.dispatch({
type: Actions.START_QOS_PING,
payload: { id }
});
};
export let dispatchNetworkUp = (edge: Edge, at = (new Date()).getTime()) => {
console.log("TODO: Insert ID HERE");
if (shouldThrottle(edge, at)) { return; }
store.dispatch(networkUp(edge, at, why));
store.dispatch(networkUp(edge, at));
bumpThrottle(edge, at);
};
export let dispatchNetworkDown = (edge: Edge, at = (new Date()).getTime(), why: string) => {
export let dispatchNetworkDown = (edge: Edge, at = (new Date()).getTime()) => {
console.log("TODO: Insert ID HERE");
if (shouldThrottle(edge, at)) { return; }
store.dispatch(networkDown(edge, at, why));
store.dispatch(networkDown(edge, at));
bumpThrottle(edge, at);
};

View File

@ -11,7 +11,7 @@ export interface ConnectionStatus {
export interface EdgeStatus {
name: Edge;
status: ConnectionStatus;
why: string;
qosPingId?: string;
}
/** Name of a connection between two points. "." can be read as "to".

View File

@ -49,6 +49,6 @@ export const onLogs =
// TODO: Make a `bot/device_123/offline` channel.
const died =
msg.message.includes("is offline") && msg.type === MessageType.error;
died && dispatchNetworkDown("bot.mqtt", undefined, "Got offline message");
died && dispatchNetworkDown("bot.mqtt");
}
};

View File

@ -1,5 +1,9 @@
import { Farmbot } from "farmbot";
import { dispatchNetworkDown, dispatchNetworkUp } from "./index";
import { Farmbot, uuid } from "farmbot";
import {
dispatchNetworkDown,
dispatchNetworkUp,
dispatchQosStart
} from "./index";
import { isNumber } from "lodash";
import axios from "axios";
import { API } from "../api/index";
@ -17,13 +21,15 @@ export function readPing(bot: Farmbot, direction: Direction): number | undefined
return isNumber(val) ? val : undefined;
}
export function markStale() {
dispatchNetworkDown("bot.mqtt", undefined, "markStale()");
export function markStale(_uuid: string) {
// dispatch({ pings: failPing(this.pingState, id) })
dispatchNetworkDown("bot.mqtt");
}
export function markActive() {
dispatchNetworkUp("user.mqtt", undefined, "markActive()");
dispatchNetworkUp("bot.mqtt", undefined, "markActive()");
export function markActive(_uuid: string) {
// dispatch({ pings: completePing(this.pingState, id) })
dispatchNetworkUp("user.mqtt");
dispatchNetworkUp("bot.mqtt");
}
export function isInactive(last: number, now: number): boolean {
@ -31,8 +37,12 @@ export function isInactive(last: number, now: number): boolean {
}
export function sendOutboundPing(bot: Farmbot) {
console.log("TODO");
bot.ping().then(markActive, markStale);
const id = uuid();
const ok = () => markActive(id);
const no = () => markStale(id);
dispatchQosStart(id);
setTimeout(no, PING_INTERVAL);
bot.ping().then(ok, no);
}
export function startPinging(bot: Farmbot) {
@ -41,7 +51,7 @@ export function startPinging(bot: Farmbot) {
}
export function pingAPI() {
const ok = () => dispatchNetworkUp("user.api", undefined, "pingApi OK");
const no = () => dispatchNetworkDown("user.api", undefined, "pingApi NO");
const ok = () => dispatchNetworkUp("user.api");
const no = () => dispatchNetworkDown("user.api");
return axios.get(API.current.devicePath).then(ok, no);
}

View File

@ -5,6 +5,7 @@ import { computeBestTime } from "./reducer_support";
import { TaggedDevice } from "farmbot";
import { SyncBodyContents } from "../sync/actions";
import { arrayUnwrap } from "../resources/util";
import { startPing, completePing, failPing } from "../devices/connectivity/qos";
export const DEFAULT_STATE: ConnectionState = {
uptime: {
@ -18,8 +19,24 @@ export const DEFAULT_STATE: ConnectionState = {
export let connectivityReducer =
generateReducer<ConnectionState>(DEFAULT_STATE)
.add<{ id: string }>(Actions.START_QOS_PING, (s, { payload }) => {
return {
...s,
pings: startPing(s.pings, payload.id)
};
})
.add<EdgeStatus>(Actions.NETWORK_EDGE_CHANGE, (s, { payload }) => {
s.uptime[payload.name] = payload.status;
const { qosPingId, status } = payload;
if (qosPingId) {
if (status.state == "up") {
console.log("OK!!!");
s.pings = completePing(s.pings, qosPingId, status.at);
} else {
console.log("FAILED");
s.pings = failPing(s.pings, qosPingId);
}
}
return s;
})
.add<SyncBodyContents<TaggedDevice>>(Actions.RESOURCE_READY, (s, a) => {
@ -33,6 +50,7 @@ export let connectivityReducer =
type Keys = (keyof ConnectionState["uptime"])[];
const keys: Keys = ["bot.mqtt", "user.mqtt", "user.api"];
keys.map(x => (s.uptime[x] = undefined));
s.pings = {};
return s;
});

View File

@ -994,5 +994,6 @@ export enum Actions {
// Network
NETWORK_EDGE_CHANGE = "NETWORK_EDGE_CHANGE",
RESET_NETWORK = "RESET_NETWORK",
SET_CONSISTENCY = "SET_CONSISTENCY"
SET_CONSISTENCY = "SET_CONSISTENCY",
START_QOS_PING = "START_QOS_PING"
}

View File

@ -111,17 +111,17 @@ describe("botReducer", () => {
step1.statusStash = "booting";
step1.hardware.informational_settings.sync_status = "synced";
const step2 = botReducer(step1, networkDown("bot.mqtt", undefined, "tests"));
const step2 = botReducer(step1, networkDown("bot.mqtt"));
expect(step2.statusStash)
.toBe(step1.hardware.informational_settings.sync_status);
expect(step2.hardware.informational_settings.sync_status).toBeUndefined();
const step3 = botReducer(step1, networkDown("bot.mqtt", undefined, "tests"));
const step3 = botReducer(step1, networkDown("bot.mqtt"));
expect(step3.statusStash)
.toBe(step1.hardware.informational_settings.sync_status);
expect(step3.hardware.informational_settings.sync_status).toBeUndefined();
const step4 = botReducer(step3, networkUp("bot.mqtt", undefined, "tests"));
const step4 = botReducer(step3, networkUp("bot.mqtt"));
expect(step4.hardware.informational_settings.sync_status)
.toBe(step3.statusStash);
});

View File

@ -3,30 +3,32 @@ import { betterCompact } from "../../util";
interface Pending {
kind: "pending";
id: string;
start: Date;
end?: undefined;
start: number;
end?: number;
}
interface Timeout {
kind: "timeout";
id: string;
start: Date;
end?: undefined;
start: number;
end?: number;
}
interface Complete {
kind: "complete";
id: string;
start: Date;
end: Date;
start: number;
end: number;
}
export type Ping = Complete | Pending | Timeout;
export type PingDictionary = Record<string, Ping | undefined>;
const now = () => (new Date()).getTime();
export const startPing =
(s: PingDictionary, id: string): PingDictionary => {
return { ...s, [id]: { kind: "pending", id, start: new Date() } };
(s: PingDictionary, id: string, start = now()): PingDictionary => {
return { ...s, [id]: { kind: "pending", id, start } };
};
export const failPing =
@ -45,7 +47,7 @@ export const failPing =
};
export const completePing =
(s: PingDictionary, id: string): PingDictionary => {
(s: PingDictionary, id: string, end = now()): PingDictionary => {
const failure = s[id];
if (failure && failure.kind == "pending") {
return {
@ -54,7 +56,7 @@ export const completePing =
kind: "complete",
id,
start: failure.start,
end: new Date()
end
}
};
}
@ -87,11 +89,8 @@ interface LatencyReport {
total: number;
}
const mapper = (p: Ping) => {
if (p.kind === "complete") {
return p.end.getTime() - p.start.getTime();
}
};
const mapper = (p: Ping) => (p.kind === "complete") ?
p.end - p.start : undefined;
export const calculateLatency =
(s: PingDictionary): LatencyReport => {

View File

@ -0,0 +1,34 @@
import {
calculateLatency,
calculatePingLoss,
PingDictionary,
} from "./qos";
import React from "react";
interface Props {
pings: PingDictionary;
}
export class QosRow extends React.Component<Props, {}> {
get pingState(): PingDictionary { return this.props.pings; }
render() {
const reportA = calculateLatency(this.pingState);
const reportB = calculatePingLoss(this.pingState);
const report = { ...reportA, ...reportB };
const ber = ((report.complete || 0) / report.total) || 0;
return <div>
<br />
<ul>
<li>best: {report.best || 0}</li>
<li>worst: {report.worst || 0}</li>
<li>average: {(report.average || 0).toFixed(1)}</li>
<li>Pings OK: {report.complete}</li>
<li>Pings pending: {report.pending || 0}</li>
<li>Pings failed: {report.timeout || 0}</li>
<li>Total pings: {report.total || 0}</li>
<li>Percent OK: {(100 * ber).toFixed(1)}</li>
</ul>
</div>;
}
}

View File

@ -12,7 +12,7 @@ import { t } from "./i18next_wrapper";
import { error } from "./toast/toast";
export function responseFulfilled(input: AxiosResponse): AxiosResponse {
dispatchNetworkUp("user.api", undefined, "responseFulfilled()");
dispatchNetworkUp("user.api");
return input;
}
@ -26,7 +26,7 @@ export const isLocalRequest = (x: SafeError) =>
let ONLY_ONCE = true;
export function responseRejected(x: SafeError | undefined) {
if (x && isSafeError(x)) {
dispatchNetworkUp("user.api", undefined, "isSafeError() REST error");
dispatchNetworkUp("user.api");
const a = ![451, 401, 422].includes(x.response.status);
const b = x.response.status > 399;
// Openfarm API was sending too many 404's.
@ -57,7 +57,7 @@ export function responseRejected(x: SafeError | undefined) {
}
return Promise.reject(x);
} else {
dispatchNetworkDown("user.api", undefined, "responseRejected");
dispatchNetworkDown("user.api");
return Promise.reject(x);
}
}