Bladeren bron

merge Distribution + Storage, show failing objects

Joystream Stats 2 jaren geleden
bovenliggende
commit
0b0f2c6c97

+ 4 - 3
src/components/Distribution/Bag.tsx

@@ -8,14 +8,15 @@ const Bag = (props: { id: number; operator: Operator; objects: Object[] }) => {
   const { id, objects, operator } = props;
   const [color, setColor] = useState(`warning`);
   const [responseTime, setResponseTime] = useState();
-  const endpoint = operator?.metadata?.nodeEndpoint;
   useEffect(() => {
     const start = moment();
-    testBag(endpoint, objects).then((color) => {
+    if (!operator?.metadata?.nodeEndpoint)
+      return console.error(`No endpoint found.`);
+    testBag(operator.metadata.nodeEndpoint, objects).then((color) => {
       setResponseTime(moment() - start);
       setColor(color);
     });
-  }, [endpoint, objects]);
+  }, [operator?.metadata?.nodeEndpoint, objects]);
   const channelId = id.split(":")[2];
   const title = objects
     ? !objects.length

+ 27 - 14
src/components/Distribution/Bucket.tsx

@@ -5,24 +5,34 @@ import Bags from "./Bags";
 import StatusBadge from "./StatusBadge";
 import { Operator, Bucket } from "./types";
 
-const BucketRow = (props: { bucket: Bucket }) => {
+const BucketRow = (props: { isDP: boolean; bucket: Bucket }) => {
   const [show, setShow] = useState(false);
-  const { id, distributing, acceptingNewBags, bags, operators } = props.bucket;
+  const { isDP, bucket } = props;
+  const { id, distributing, acceptingNewBags, bags, operatorMetadata } = bucket;
+  const operator = isDP ? bucket.operators[0] : { metadata: operatorMetadata };
   return (
     <>
       <div key={id} className="d-flex flex-row" onClick={() => setShow(!show)}>
-        <h3>{id}</h3>
-        <StatusBadge status={distributing} label={"D"} title={"distributing"} />
+        <div className="col-1 text-right d-flex justify-content-between">
+          <h3>{id}</h3>
+          {isDP && (
+            <StatusBadge
+              status={distributing}
+              label={"D"}
+              title={(distributing ? "" : "not ") + "distributing"}
+            />
+          )}
+        </div>
         <StatusBadge
           status={acceptingNewBags}
           label={"A"}
-          title={"accepting new bags"}
+          title={(acceptingNewBags ? "" : "not ") + "accepting new bags"}
         />
-        <div className="col-1 p-2">{bags.length} bags</div>
-        <OperatorFields operator={operators[0]} />
+        <div className="col-1">{bags.length} bags</div>
+        <OperatorFields operator={operator} />
       </div>
       {show ? (
-        <Bags show={show} bucketId={id} bags={bags} operator={operators[0]} />
+        <Bags show={show} bucketId={id} bags={bags} operator={operator} />
       ) : (
         ``
       )}
@@ -33,17 +43,20 @@ const BucketRow = (props: { bucket: Bucket }) => {
 export default BucketRow;
 
 const OperatorFields = (props: { operator: Operator }) => {
-  if (!props.operator) return <div className="col-7" />;
+  if (!props.operator) return <div className="col-7">No operator info.</div>;
   const { workerId, member, metadata } = props.operator;
-  const statusUrl = metadata?.nodeEndpoint
-    ? metadata.nodeEndpoint + `api/v1/status`
-    : ``;
   return (
     <>
-      <div className="col-1" title={`worker ${workerId}`}>
+      <div
+        className="col-1"
+        title={workerId ? `worker ${workerId}` : `No worker info.`}
+      >
         {member ? <Badge>{member.handle}</Badge> : ``}
       </div>
-      <Badge title={statusUrl} className="col-3 text-left">
+      <Badge
+        title={metadata?.nodeEndpoint || `No metadata set.`}
+        className="col-3 text-left overflow-hidden"
+      >
         {metadata?.extra}
       </Badge>
       <Metadata metadata={metadata} />

+ 21 - 5
src/components/Distribution/Status.tsx

@@ -14,17 +14,33 @@ const Status = (props: { endpoint: string }) => {
       .then(({ data }) => setStatus(data))
       .catch((e) => console.log(`status`, url, e.message));
   };
-  useEffect(() => updateStatus(endpoint + `api/v1/status`), [endpoint]);
-
+  useEffect(() => {
+    const route = endpoint.includes("/distributor/") ? "status" : "state/data";
+    updateStatus(endpoint + "api/v1/" + route);
+  }, [endpoint]);
+  if (status.totalSize)
+    return (
+      <>
+        <Badge className="col-1 text-right" title="Storage used in GB">
+          {gb(status.totalSize)} gb
+        </Badge>
+        <Badge className="col-1" title="Files (downloading)">
+          {status.objectNumber} files ({status.tempDownloads})
+        </Badge>
+        <Badge className="col-1" title="temp dir size">
+          temp: {gb(status.tempDirSize)} gb
+        </Badge>
+      </>
+    );
   if (!status.id) return <div />;
   const { id, objectsInCache, storageLimit, storageUsed, uptime } = status;
   const upSince = moment().subtract(uptime * 1000);
   return (
     <>
-      <Badge className="col-1" title="GB used / limit">
-        {gb(storageUsed)}/{gb(storageLimit)}
+      <Badge className="col-1 text-right" title="GB used / limit">
+        {gb(storageUsed)}/{gb(storageLimit)} gb
       </Badge>
-      <Badge className="col-1" title="Objects (downloading)">
+      <Badge className="col-1" title="Files (downloading)">
         {objectsInCache} ({status.downloadsInProgress})
       </Badge>
       <Badge className="col-1" title="up since">

+ 95 - 0
src/components/Distribution/TestResults.tsx

@@ -0,0 +1,95 @@
+import { useState } from "react";
+import moment from "moment";
+import { Button } from "react-bootstrap";
+
+const Results = (props: { results: any[] }) => {
+  const { results } = props;
+  if (!results.length) return <div />;
+
+  // TODO move to parent
+  let providers = [];
+  results.forEach((r) => {
+    if (!providers[r.endpoint]) providers[r.endpoint] = [];
+    providers[r.endpoint].push(r);
+  });
+
+  return (
+    <div className="mt-2">
+      <h2>Failing Objects</h2>
+      <div className="d-flex flex-column">
+        {Object.keys(providers)
+          .sort((a, b) => providers[b].length - providers[a].length)
+          .map((url) => (
+            <Provider key={url} url={url} results={providers[url]} />
+          ))}
+      </div>
+    </div>
+  );
+};
+
+export default Results;
+
+const Provider = (props: { results: any[] }) => {
+  const [expand, setExpand] = useState(false);
+  const { url, results } = props;
+  const totalLatency = results.reduce((sum, r) => +sum + +r.latency, 0);
+  const avgLatency = totalLatency / results.length;
+  return (
+    <div className="d-flex flex-column" onClick={() => setExpand(!expand)}>
+      <div className="d-flex flex-row mx-2 p-1">
+        <Button
+          variant={expand ? "light" : "dark"}
+          className="col-5"
+          title="click to expand"
+        >
+          {url}
+        </Button>
+        <Button variant="danger" className="col-1 mx-1">
+          {results.length} errors
+        </Button>
+        <Button variant="success" className="col-1">
+          {avgLatency.toFixed()} ms
+        </Button>
+      </div>
+
+      <div className="d-flex flex-column px-2">
+        {expand &&
+          results.slice(0, 1000).map((r) => (
+            <div key={r.id} className="d-flex flex-row">
+              <Button variant="dark" className="col-2 m-1">
+                {moment(r.createdAt).format(`DD-MMM-YY HH:mm:ss.SSS`)}
+              </Button>
+              <Button
+                variant="danger"
+                className="col-1 p-0 m-1"
+                title="open in a new tab to see original response"
+              >
+                <a href={r.url}>{r.objectId}</a>
+              </Button>
+              <Button
+                variant="warning"
+                className="col-5 m-1"
+                title="error message"
+              >
+                {r.status}
+              </Button>
+              <Button
+                variant="success"
+                className="col-1 m-1"
+                title="server responded within this time"
+              >
+                {r.latency}ms
+              </Button>
+              <Button
+                variant="light"
+                className="col-2 m-1"
+                title="click to see on the map where requests where made from (not yet implemented)"
+              >
+                {r.origin}
+              </Button>
+            </div>
+          ))}
+      </div>
+    </div>
+  );
+};

+ 35 - 11
src/components/Distribution/index.tsx

@@ -1,28 +1,52 @@
+import axios from "axios";
 import { useState, useEffect } from "react";
 import { Loading } from "..";
 import Bucket from "./Bucket";
-import { getDistributionBuckets } from "./util";
+import TestResults from "./TestResults";
+import { getBuckets } from "./util";
 
 const Distribution = (props: {
   workers: { distributionWorkingGroup?: Worker[] };
 }) => {
-  const [buckets, setBuckets] = useState([]);
+  const { workers } = props;
+  const [sBuckets, setSBuckets] = useState([]);
+  const [dBuckets, setDBuckets] = useState([]);
+  const [results, setResults] = useState([]);
   useEffect(() => {
     const update = () =>
-      getDistributionBuckets(props.workers?.distributionWorkingGroup).then(
-        (buckets) => buckets.length && setBuckets(buckets)
-      );
+      getBuckets([
+        workers?.storageWorkingGroup,
+        workers?.distributionWorkingGroup,
+      ]).then((buckets) => {
+        if (buckets[0].length) setSBuckets(buckets[0]);
+        if (buckets[1].length) setDBuckets(buckets[1]);
+      });
     update();
-    setTimeout(update, 30000);
-  }, [props.workers?.distributionWorkingGroup]);
+    setTimeout(update, 10000);
+    axios
+      .get(`https://joystreamstats.live/api/v1/bags/errors`)
+      .then(({ data }) =>
+        setResults(data.sort((a, b) => a.timestamp - b.timestamp))
+      );
+  }, [workers?.storageWorkingGroup, workers?.distributionWorkingGroup]);
   return (
     <div className="m-2 p-2 bg-light">
-      <h2>Distribution Providers</h2>
-      {buckets.length ? (
-        buckets.map((bucket) => <Bucket key={bucket.id} bucket={bucket} />)
+      <h2>Storage Buckets</h2>
+      {sBuckets.length ? (
+        sBuckets.map((bucket) => <Bucket key={bucket.id} bucket={bucket} />)
+      ) : (
+        <Loading target="storage buckets" />
+      )}
+
+      <h2>Distribution Buckets</h2>
+      {dBuckets.length ? (
+        dBuckets.map((bucket) => (
+          <Bucket key={bucket.id} bucket={bucket} isDP={true} />
+        ))
       ) : (
-        <Loading target="buckets" />
+        <Loading target="distribution buckets" />
       )}
+      <TestResults results={results} />
     </div>
   );
 };

+ 8 - 2
src/components/Distribution/queries.ts

@@ -1,4 +1,10 @@
-export const qnDistributionBuckets = `query distributionBuckets { distributionBuckets{id,createdAt,distributing,acceptingNewBags,operators {workerId,metadata {extra,nodeEndpoint,nodeLocation {countryCode,coordinates {longitude,latitude}}}} bags{id}}}`;
+export const qnBuckets = `query {
+  distributionBuckets{id,createdAt,distributing,acceptingNewBags,bags{id}
+    operators {workerId,metadata {extra,nodeEndpoint,nodeLocation {countryCode,coordinates {longitude,latitude}}}}
+  } storageBuckets {id,createdAt,acceptingNewBags,bags{id}
+    operatorMetadata {extra,nodeEndpoint,nodeLocation {countryCode,coordinates {longitude,latitude}}}
+  }}`;
 
 export const qnBucketObjects = (bucketId: string) =>
-  `query { distributionBuckets(where: { id_eq: "${bucketId}" }) {id,bags {id,objects {id, size}}}}`;
+  `query {  storageBuckets(where: { id_eq: "${bucketId}" }) {id,bags {id,objects {id, size}}}
+  distributionBuckets(where: { id_eq: "${bucketId}" }) {id,bags {id,objects {id, size}}} }`;

+ 2 - 2
src/components/Distribution/types.ts

@@ -1,6 +1,6 @@
 export interface Operator {
-  workerId: number;
-  metadata: {
+  workerId?: number;
+  metadata?: {
     extra: string;
     nodeEndpoint: string;
   };

+ 35 - 24
src/components/Distribution/util.ts

@@ -1,12 +1,12 @@
 import axios from "axios";
 import { queryNode } from "../../config";
 import { Bucket } from "./types";
-import { qnDistributionBuckets, qnBucketObjects } from "./queries";
+import { qnBuckets, qnBucketObjects } from "./queries";
 
 export const gb = (bytes: number) => (bytes / 1024 ** 3).toFixed() + `gb`;
 
 const fail = (msg: string) => {
-  console.log(`getQN: ${msg}`);
+  console.log(`postQN: ${msg}`);
   return [];
 };
 export const postQN = (query) =>
@@ -22,11 +22,13 @@ export const postQN = (query) =>
 export const testBag = async (
   endpoint: string,
   objects: Object[] | null
-): Promise<[string, number]> => {
+): Promise<[string]> => {
+  if (!endpoint) return `no endpoint given`;
   if (!objects) return `warning`;
   if (!objects.length) return ``;
   const object = Math.round(Math.random() * (objects.length - 1));
-  const url = endpoint + `api/v1/assets/${objects[object].id}`;
+  const route = endpoint.includes(`storage`) ? `files` : `assets`;
+  const url = endpoint + `api/v1/${route}/${objects[object].id}`;
   return axios
     .head(url)
     .then((data) => `success`)
@@ -37,25 +39,32 @@ export const testBag = async (
 };
 
 export const getBucketObjects = async (bucketId: number) =>
-  postQN(qnBucketObjects(bucketId)).then(({ distributionBuckets }) => {
-    if (!distributionBuckets) {
-      console.error(`getBucketObjects: received empty distributionBuckets`);
-      return [];
+  postQN(qnBucketObjects(bucketId)).then(
+    ({ storageBuckets, distributionBuckets }) => {
+      const bags = storageBuckets.length
+        ? storageBuckets[0].bags
+        : distributionBuckets[0].bags;
+      console.debug(`received objects of bucket`, bucketId, bags);
+      return bags;
     }
-    const bags = distributionBuckets[0].bags;
-    console.debug(`received objects of bucket`, bucketId, bags);
-    return bags;
-  });
-
-export const getDistributionBuckets = async (
-  workers?: Worker[]
-): Promise<Bucket[]> =>
-  postQN(qnDistributionBuckets).then(({ distributionBuckets }) =>
-    sortBuckets(
-      distributionBuckets.map((b) => addWorkerMemberships(b, workers))
-    )
   );
 
+export const getBuckets = async (workers: Worker[][]): Promise<Bucket[]> =>
+  postQN(qnBuckets).then(({ distributionBuckets, storageBuckets }) => {
+    if (!storageBuckets || !distributionBuckets) return [[], []];
+    const storage =
+      storageBuckets &&
+      sortBuckets(
+        storageBuckets.map((b) => addWorkerMemberships(b, workers[0]))
+      );
+    const distribution =
+      distributionBuckets &&
+      sortBuckets(
+        distributionBuckets.map((b) => addWorkerMemberships(b, workers[1]))
+      );
+    return [storage, distribution];
+  });
+
 // TODO OPTIMIZE sort by bucketIndex
 const sortBuckets = (buckets: Bucket[]): Bucket[] =>
   buckets.sort((a, b) => a.id.split(":")[0] - b.id.split(":")[0]);
@@ -64,9 +73,11 @@ export const addWorkerMemberships = (
   bucket: Bucket,
   workers?: Worker[]
 ): Bucket => {
-  const operators = bucket.operators.map((operator) => {
-    const member = workers?.find((w) => w.id === operator.workerId);
-    return { ...operator, member };
-  });
+  let operators = [];
+  if (bucket.operators)
+    operators = bucket.operators.map((operator) => {
+      const member = workers?.find((w) => w.id === operator.workerId);
+      return { ...operator, member };
+    });
   return { ...bucket, operators };
 };

+ 4 - 2
src/components/Routes/index.tsx

@@ -19,7 +19,7 @@ const Spending = React.lazy(() => import("../Proposals/Spending"));
 const Timeline = React.lazy(() => import("../Timeline"));
 const Tokenomics = React.lazy(() => import("../Tokenomics"));
 const Validators = React.lazy(() => import("../Validators"));
-const Storage = React.lazy(() => import("../Storage"));
+//const Storage = React.lazy(() => import("../Storage"));
 const Distribution = React.lazy(() => import("../Distribution"));
 const Transactions = React.lazy(() => import("../Transactions"));
 const Bounties = React.lazy(() => import("../Bounties"));
@@ -132,7 +132,9 @@ const Routes = (props: IProps) => {
               />
               <Route
                 path="/storage"
-                render={(routeprops) => <Storage {...routeprops} {...props} />}
+                render={(routeprops) => (
+                  <Distribution {...routeprops} {...props} />
+                )}
               />
               <Route
                 path="/distribution"