Merge pull request #1942 from molecula/fb-1127

rip out ui/usage
This commit is contained in:
reesporte 2022-02-28 13:16:29 -06:00 committed by GitHub
commit da3ce449ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 33 additions and 1281 deletions

291
api.go
View file

@ -50,7 +50,6 @@ type API struct {
importWorkerPoolSize int
importWork chan importJob
usageCache *usageCache
schemaDetailsOn bool
Serializer Serializer
@ -938,260 +937,6 @@ func (api *API) PrimaryNode() *topology.Node {
return snap.PrimaryFieldTranslationNode()
}
// Cache of disk usage statistics
type usageCache struct {
data map[string]NodeUsage
refreshInterval time.Duration
lastUpdated time.Time
resetTrigger chan bool
lastCalcDuration time.Duration
waitMultiplier float64
disable bool
muCalculate sync.Mutex
muAssign sync.Mutex
}
var usageCacheMinDuration = 5 * time.Second // If usage takes less than this duration to calculate, don't use the cache.
var usageCacheMinInterval = time.Hour // Refresh interval is forced to be >= this duration.
var usageCacheInitialInterval = time.Hour // Refresh interval starts with this duration.
// NodeUsage represents all usage measurements for one node.
type NodeUsage struct {
Disk DiskUsage `json:"diskUsage"`
Memory MemoryUsage `json:"memoryUsage"`
LastUpdated time.Time `json:"lastUpdated"`
}
// DiskUsage represents the storage space used on disk by one node.
type DiskUsage struct {
Capacity uint64 `json:"capacity,omitempty"`
TotalUse uint64 `json:"totalInUse"`
IndexUsage map[string]IndexUsage `json:"indexes"`
}
// IndexUsage represents the storage space used on disk by one index, on one node.
type IndexUsage struct {
Total uint64 `json:"total"`
IndexKeys uint64 `json:"indexKeys"`
FieldKeysTotal uint64 `json:"fieldKeysTotal"`
Fragments uint64 `json:"fragments"`
Metadata uint64 `json:"metadata"`
Fields map[string]FieldUsage `json:"fields"`
}
// FieldUsage represents the storage space used on disk by one field, on one node
type FieldUsage struct {
Total uint64 `json:"total"`
Fragments uint64 `json:"fragments"`
Keys uint64 `json:"keys"`
Metadata uint64 `json:"metadata"`
}
// MemoryUsage represents the memory used by one node.
type MemoryUsage struct {
Capacity uint64 `json:"capacity"`
TotalUse uint64 `json:"totalInUse"`
}
// Returns disk usage from cache if cache is large. It will recalculate on the spot if the last cacluation was under 5 seconds.
func (api *API) Usage(ctx context.Context, remote bool) (map[string]NodeUsage, error) {
span, _ := tracing.StartSpanFromContext(ctx, "API.Usage")
defer span.Finish()
if api.usageCache.disable {
resp := make(map[string]NodeUsage)
return resp, nil
}
api.usageCache.muAssign.Lock()
lastCalc := api.usageCache.lastCalcDuration
api.usageCache.muAssign.Unlock()
if lastCalc < usageCacheMinDuration {
err := api.ResetUsageCache()
if err != nil {
api.server.logger.Infof("could not reset usageCache: %s", err)
}
}
api.usageCache.muAssign.Lock()
lastUpdated := api.usageCache.lastUpdated
api.usageCache.muAssign.Unlock()
if lastUpdated == (time.Time{}) {
api.calculateUsage()
}
if !remote {
api.requestUsageOfNodes()
}
return api.usageCache.data, nil
}
// Makes a ui/usage request for each node in cluster to calculates its usage and adds it to the cache
func (api *API) requestUsageOfNodes() {
nodes := api.cluster.Nodes()
for _, node := range nodes {
if node.ID == api.server.nodeID {
continue
}
nodeUsage, err := api.server.defaultClient.GetNodeUsage(context.Background(), &node.URI)
if err != nil {
api.server.logger.Infof("couldn't collect disk usage from %s: %s", node.URI, err)
}
api.usageCache.muAssign.Lock()
api.usageCache.data[node.ID] = nodeUsage[node.ID]
api.usageCache.muAssign.Unlock()
}
}
// Calculates disk usage from scratch if cache has expired for each index and stores the results in the usage cache
func (api *API) calculateUsage() {
// don't need to calculateUsage if we're about to close!
if api.isClosing() {
return
}
api.usageCache.muCalculate.Lock()
defer api.usageCache.muCalculate.Unlock()
if ok := api.server.addToWaitGroup(1); !ok {
// the server is closing, so just stop!
return
}
defer api.server.wg.Done()
api.usageCache.muAssign.Lock()
lastUpdated := api.usageCache.lastUpdated
api.usageCache.muAssign.Unlock()
if time.Since(lastUpdated) <= api.usageCache.refreshInterval {
return
}
indexDetails, nodeMetadataBytes, err := api.holder.Txf().IndexUsageDetails(api.isClosing)
if err != nil {
api.server.logger.Infof("couldn't get index usage details: %s", err)
}
totalSize := nodeMetadataBytes
for _, s := range indexDetails {
totalSize += s.Total
}
// NOTE: these errors are ignored in api.Info(), but checked here
si := api.server.systemInfo
diskCapacity, err := si.DiskCapacity(api.holder.path)
if err != nil {
api.server.logger.Infof("couldn't read disk capacity: %s", err)
}
memoryCapacity, err := si.MemTotal()
if err != nil {
api.server.logger.Infof("couldn't read memory capacity: %s", err)
}
memoryUse, err := si.MemUsed()
if err != nil {
api.server.logger.Infof("couldn't read memory usage: %s", err)
}
lastUpdated = time.Now()
// Insert into result.
nodeUsage := NodeUsage{
Disk: DiskUsage{
Capacity: diskCapacity,
TotalUse: totalSize,
IndexUsage: indexDetails,
},
Memory: MemoryUsage{
Capacity: memoryCapacity,
TotalUse: memoryUse,
},
LastUpdated: lastUpdated,
}
api.usageCache.muAssign.Lock()
api.usageCache.data = make(map[string]NodeUsage)
api.usageCache.data[api.server.nodeID] = nodeUsage
api.usageCache.lastUpdated = lastUpdated
api.usageCache.muAssign.Unlock()
}
// Periodically calculates disk/memory usage in terms of the duty cycle. The duty cycle represents the percentage of
// time that is spent recalculating this cache. It is specified relatively, rather than by a set interval, because
// scans can take an unpredictably long time.
func (api *API) RefreshUsageCache(dutyCycle float64) {
if dutyCycle == 0 {
api.server.logger.Warnf("usage-duty-cycle set to 0, usage cache and /ui/usage endpoint are disabled")
api.usageCache = &usageCache{
disable: true,
}
return
}
trigger := make(chan bool)
defer close(trigger)
multiplier := 100/dutyCycle - 1
api.usageCache = &usageCache{
data: make(map[string]NodeUsage),
refreshInterval: usageCacheInitialInterval,
resetTrigger: trigger,
lastCalcDuration: 0,
waitMultiplier: multiplier,
}
api.server.logger.Infof("monitoring resource usage with duty cycle %v%%\n", dutyCycle)
for {
start := time.Now()
api.calculateUsage()
api.setRefreshInterval(time.Since(start))
api.server.logger.Infof("updated resource usage cache at %v, took %v, next update in %v\n", api.usageCache.lastUpdated.Format(time.RFC3339), api.usageCache.lastCalcDuration.Truncate(time.Millisecond), api.usageCache.refreshInterval.Truncate(100*time.Millisecond))
select {
case <-trigger:
continue
case <-api.server.closing:
return
case <-time.After(api.usageCache.refreshInterval):
continue
}
}
}
// Refresh interval set in relation to how long the last calculation took.
func (api *API) setRefreshInterval(dur time.Duration) {
refresh := time.Duration(float64(dur) * api.usageCache.waitMultiplier)
if refresh < usageCacheMinInterval {
refresh = usageCacheMinInterval
}
api.usageCache.muAssign.Lock()
api.usageCache.refreshInterval = refresh
api.usageCache.lastCalcDuration = dur
api.usageCache.muAssign.Unlock()
}
// Resets the lastUpdated time and awakens RefreshUsageCache()
func (api *API) ResetUsageCache() error {
if api.usageCache != nil {
api.usageCache.muAssign.Lock()
api.usageCache.lastUpdated = time.Time{}
api.usageCache.muAssign.Unlock()
} else {
return errors.New("invalidating cache: cache not initialized")
}
api.usageCache.resetTrigger <- true
return nil
}
// isClosing returns true if the server is shutting down.
func (api *API) isClosing() bool {
select {
case <-api.server.closing:
return true
default:
return false
}
}
// RecalculateCaches forces all TopN caches to be updated.
// This is done internally within a TopN query, but a user may want to do it ahead of time?
func (api *API) RecalculateCaches(ctx context.Context) error {
@ -3256,24 +3001,24 @@ var methodsResizing = map[apiMethod]struct{}{
apiSchema: {},
}
var methodsDegraded = map[apiMethod]struct{}{
apiExportCSV: {},
apiFragmentBlockData: {},
apiFragmentBlocks: {},
apiField: {},
apiIndex: {},
apiQuery: {},
apiRecalculateCaches: {},
apiRemoveNode: {},
apiShardNodes: {},
apiSchema: {},
apiViews: {},
apiStartTransaction: {},
apiFinishTransaction: {},
apiTransactions: {},
apiGetTransaction: {},
apiActiveQueries: {},
}
// var methodsDegraded = map[apiMethod]struct{}{
// apiExportCSV: {},
// apiFragmentBlockData: {},
// apiFragmentBlocks: {},
// apiField: {},
// apiIndex: {},
// apiQuery: {},
// apiRecalculateCaches: {},
// apiRemoveNode: {},
// apiShardNodes: {},
// apiSchema: {},
// apiViews: {},
// apiStartTransaction: {},
// apiFinishTransaction: {},
// apiTransactions: {},
// apiGetTransaction: {},
// apiActiveQueries: {},
// }
var methodsNormal = map[apiMethod]struct{}{
apiCreateField: {},

View file

@ -90,9 +90,6 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
flags.Uint16Var(&srv.Config.Postgres.ConnectionLimit, "postgres.connection-limit", srv.Config.Postgres.ConnectionLimit, "Maximum number of simultaneous postgres connections to allow. (set 0 to disable)")
flags.Uint16Var(&srv.Config.Postgres.SqlVersion, "postgres.sql-version", srv.Config.Postgres.SqlVersion, "Molecula Sql Handling Version (default 1)")
// Disk and Memory usage cache for ui/usage endpoint
flags.Float64Var(&srv.Config.UsageDutyCycle, "usage-duty-cycle", srv.Config.UsageDutyCycle, "Sets the percentage of time that is spent recalculating the disk and memory usage cache. 100.0 for always-running, 0 disables the cache and the /ui/usage endpoint.")
// Future flags.
flags.BoolVar(&srv.Config.Future.Rename, "future.rename", false, "Present application name as FeatureBase. Defaults to false, will default to true in an upcoming release.")

View file

@ -452,7 +452,6 @@ func newRouter(handler *Handler) http.Handler {
router.HandleFunc("/version", handler.handleGetVersion).Methods("GET").Name("GetVersion")
// /ui endpoints are for UI use; they may change at any time.
router.HandleFunc("/ui/usage", handler.chkAuthZ(handler.handleGetUsage, authz.Read)).Methods("GET").Name("GetUsage")
router.HandleFunc("/ui/transaction", handler.chkAuthZ(handler.handleGetTransactionList, authz.Read)).Methods("GET").Name("GetTransactionList")
router.HandleFunc("/ui/transaction/", handler.chkAuthZ(handler.handleGetTransactionList, authz.Read)).Methods("GET").Name("GetTransactionList")
router.HandleFunc("/ui/shard-distribution", handler.chkAuthZ(handler.handleGetShardDistribution, authz.Admin)).Methods("GET").Name("GetShardDistribution")
@ -987,63 +986,6 @@ func (h *Handler) handlePostSchema(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// handleGetUsage handles GET /ui/usage requests.
func (h *Handler) handleGetUsage(w http.ResponseWriter, r *http.Request) {
if !validHeaderAcceptJSON(r.Header) {
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
return
}
q := r.URL.Query()
remoteStr := q.Get("remote")
var remote bool
if remoteStr == "true" {
remote = true
}
nodeUsages, err := h.api.Usage(r.Context(), remote)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// if auth is turned on, filter results
if h.auth != nil {
g := r.Context().Value(contextKeyGroupMembership)
if g == nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
if !h.permissions.IsAdmin(g.([]authn.Group)) {
allowed := h.permissions.GetAuthorizedIndexList(g.([]authn.Group), authz.Read)
filteredNodeUsages := map[string]NodeUsage{}
for nodeId, nodeUsage := range nodeUsages {
filteredIndexUsage := NodeUsage{
Disk: DiskUsage{
IndexUsage: map[string]IndexUsage{},
},
}
for index, idxUsage := range nodeUsage.Disk.IndexUsage {
// is it in auth list
for _, authd := range allowed {
if index == authd {
filteredIndexUsage.Disk.IndexUsage[index] = idxUsage
break
}
}
}
filteredNodeUsages[nodeId] = filteredIndexUsage
}
nodeUsages = filteredNodeUsages
}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(nodeUsages); err != nil {
h.logger.Errorf("write status response error: %s", err)
}
}
// handleGetShardDistribution handles GET /ui/shard-distribution requests.
func (h *Handler) handleGetShardDistribution(w http.ResponseWriter, r *http.Request) {
dist := h.api.ShardDistribution(r.Context())

View file

@ -244,28 +244,6 @@ log-path = "/var/log/molecula/featurebase.log"
# enable-client-verification = true
# ==============================================================================
# Usage Duty Cycle - Featurebase maintains a disk/memory usage cache that is
# calculated periodically in the background and accessed by the UI/usage
# endpoint. Since this disk scan can take a long and unpredictable amount of
# time, its timing behavior is specified in a relative, rather than absolute
# sense. That is, the duty cycle sets the percentage of time that is spent
# recalculating this cache. This setting affects the results received from
# the "/ui/usage" http endpoint, as well as all data file and memory usage
# values and graphs on the webui "tables" page
# Special considerations:
# * If disk usage can be calculated quickly (less than 5 seconds), fresh
# results will be calculated when accessed
# * When disk usage takes longer to calculate, there is a minimum of one
# hour wait between cache recalculations
# Setting this value to 0 will completely disable the calculation of disk usage
#
# usage-duty-cycle = 20
# ==============================================================================
# Use [metric] stanza to define attributes for monitoring.
# [metric]

View file

@ -244,28 +244,6 @@
# enable-client-verification = true
# ==============================================================================
# Usage Duty Cycle - Featurebase maintains a disk/memory usage cache that is
# calculated periodically in the background and accessed by the UI/usage
# endpoint. Since this disk scan can take a long and unpredictable amount of
# time, its timing behavior is specified in a relative, rather than absolute
# sense. That is, the duty cycle sets the percentage of time that is spent
# recalculating this cache. This setting affects the results received from
# the "/ui/usage" http endpoint, as well as all data file and memory usage
# values and graphs on the webui "tables" page
# Special considerations:
# * If disk usage can be calculated quickly (less than 5 seconds), fresh
# results will be calculated when accessed
# * When disk usage takes longer to calculate, there is a minimum of one
# hour wait between cache recalculations
# Setting this value to 0 will completely disable the calculation of disk usage
#
# usage-duty-cycle = 20
# ==============================================================================
# Use [metric] stanza to define attributes for monitoring.
# [metric]

View file

@ -1383,38 +1383,6 @@ func (c *InternalClient) TranslateIDsNode(ctx context.Context, uri *pnet.URI, in
return tkresp.Keys, nil
}
// GetNodeUsage retrieves the size-on-disk information for the specified node.
func (c *InternalClient) GetNodeUsage(ctx context.Context, uri *pnet.URI) (map[string]NodeUsage, error) {
u := uri.Path("/ui/usage?remote=true")
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return nil, errors.Wrap(err, "creating request")
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "pilosa/"+Version)
req = AddAuthToken(ctx, req)
// Execute request against the host.
resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Read body and unmarshal response.
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "reading")
}
nodeUsages := make(map[string]NodeUsage) // map of size 1
if err := json.Unmarshal(body, &nodeUsages); err != nil {
return nil, fmt.Errorf("unmarshal response: %s", err)
}
return nodeUsages, nil
}
// GetPastQueries retrieves the query history log for the specified node.
func (c *InternalClient) GetPastQueries(ctx context.Context, uri *pnet.URI) ([]PastQueryStatus, error) {
u := uri.Path("/query-history?remote=true")

View file

@ -19,14 +19,12 @@ export const ClusterHealth: FC = () => {
const [cluster, setCluster] = useState<any>();
const [metrics, setMetrics] = useState<any>();
const [info, setInfo] = useState<any>();
const [clusterData, setClusterData] = useState<any>();
const [expanded, setExpanded] = useState<string[]>([]);
const [showMetrics, setShowMetrics] = useState<any>();
const allExpanded = cluster && expanded.length === cluster.nodes.length;
useEffectOnce(() => {
getClusterHealth();
getClusterData();
});
const refreshMetrics = useCallback(() => {
@ -38,15 +36,11 @@ export const ClusterHealth: FC = () => {
useEffect(() => {
const interval = setInterval(() => {
if (!clusterData) {
getClusterData();
}
getClusterHealth();
refreshMetrics();
}, 15000);
return () => clearInterval(interval);
}, [refreshMetrics, cluster, clusterData]);
}, [refreshMetrics, cluster]);
const getClusterHealth = () => {
pilosa.get
@ -76,13 +70,6 @@ export const ClusterHealth: FC = () => {
.catch(() => setMetrics(undefined));
};
const getClusterData = () => {
pilosa.get
.usage()
.then((res) => setClusterData(res.data))
.catch(() => setClusterData(undefined));
};
const toggleAccordion = (nodeId: string) => {
const isExpanded = expanded.includes(nodeId);
if (isExpanded) {
@ -140,7 +127,6 @@ export const ClusterHealth: FC = () => {
key={node.id}
node={node}
info={info}
usage={clusterData ? clusterData[node.id] : undefined}
expanded={expanded.includes(node.id)}
onToggle={() => toggleAccordion(node.id)}
onMetricClick={() => setShowMetrics(node)}

View file

@ -1,4 +1,4 @@
import React, { FC, Fragment, useState } from 'react';
import React, { FC, useState } from 'react';
import Button from '@material-ui/core/Button';
import copy from 'copy-to-clipboard';
import EqualizerIcon from '@material-ui/icons/EqualizerSharp';
@ -11,7 +11,6 @@ import Find from 'lodash/find';
import IconButton from '@material-ui/core/IconButton';
import InfoIcon from '@material-ui/icons/Info';
import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import { formatBytes } from 'shared/utils/formatBytes';
import { nodeInfo } from './nodeInfo';
import { NODE_STATE } from './nodeStatus';
@ -21,7 +20,6 @@ import css from './Node.module.scss';
type NodeType = {
node: any;
info: any;
usage: any;
expanded: boolean;
onToggle: () => void;
onMetricClick: () => void;
@ -30,24 +28,13 @@ type NodeType = {
export const Node: FC<NodeType> = ({
node,
info,
usage,
expanded,
onToggle,
onMetricClick
onMetricClick,
}) => {
const [copyHost, setCopyHost] = useState<string>('Copy Host');
const [copyID, setCopyID] = useState<string>('Click to Copy');
const { id, isPrimary, state } = node;
const diskTotalInUse = usage?.diskUsage?.totalInUse;
const diskCapacity = usage?.diskUsage?.capacity;
const diskUsagePercentage = diskCapacity
? (diskTotalInUse / diskCapacity) * 100
: undefined;
const memoryTotalInUse = usage?.memoryUsage?.totalInUse;
const memoryCapacity = usage?.memoryUsage?.capacity;
const memoryUsagePercentage = memoryCapacity
? (memoryTotalInUse / memoryCapacity) * 100
: undefined;
const keys = Object.keys(info);
const onCopyHostClick = () => {
@ -103,154 +90,6 @@ export const Node: FC<NodeType> = ({
</span>
</Tooltip>
</div>
<div className={css.nodeUsage}>
<div>
<div className={css.label}>Disk Usage:</div>
<div>
{usage ? (
<Fragment>
<Typography variant="caption">
{formatBytes(diskTotalInUse)}
{diskCapacity
? ` used out of ${formatBytes(diskCapacity)}`
: null}
</Typography>
<div className={css.totalCapacity}>
{diskUsagePercentage ? (
<Tooltip
title={
<Typography variant="caption">
{diskUsagePercentage < 1
? '< 1'
: diskUsagePercentage.toLocaleString(
undefined,
{ maximumFractionDigits: 1 }
)}
% used
</Typography>
}
placement="top"
arrow
>
<div
className={css.totalInUse}
style={{
width: `${
diskUsagePercentage < 1
? 1
: diskUsagePercentage
}%`
}}
/>
</Tooltip>
) : (
<Fragment>
<Tooltip
title={
<Typography variant="caption">
{formatBytes(diskTotalInUse)} used
</Typography>
}
placement="top"
arrow
>
<div
className={css.totalInUse}
style={{ width: '2%' }}
/>
</Tooltip>
<Typography
className={css.unknownCapacity}
variant="caption"
color="textSecondary"
>
Node disk capacity unknown
</Typography>
</Fragment>
)}
</div>
</Fragment>
) : (
<Typography variant="caption" paragraph>
Calculating...
</Typography>
)}
</div>
</div>
<div>
<div className={css.label}>Memory Usage:</div>
<div>
{usage ? (
<Fragment>
<Typography variant="caption">
{formatBytes(memoryTotalInUse)}
{memoryCapacity
? ` used out of ${formatBytes(memoryCapacity)}`
: null}
</Typography>
<div className={css.totalCapacity}>
{memoryUsagePercentage ? (
<Tooltip
title={
<Typography variant="caption">
{memoryUsagePercentage < 1
? '< 1'
: memoryUsagePercentage.toLocaleString(
undefined,
{ maximumFractionDigits: 1 }
)}
% used
</Typography>
}
placement="top"
arrow
>
<div
className={css.totalInUse}
style={{
width: `${
memoryUsagePercentage < 1
? 1
: memoryUsagePercentage
}%`
}}
/>
</Tooltip>
) : (
<Fragment>
<Tooltip
title={
<Typography variant="caption">
{formatBytes(memoryTotalInUse)} used
</Typography>
}
placement="top"
arrow
>
<div
className={css.totalInUse}
style={{ width: '2%' }}
/>
</Tooltip>
<Typography
className={css.unknownCapacity}
variant="caption"
color="textSecondary"
>
Node memory capacity unknown
</Typography>
</Fragment>
)}
</div>
</Fragment>
) : (
<Typography variant="caption" paragraph>
Calculating...
</Typography>
)}
</div>
</div>
</div>
<div className={css.nodeSettings}>
{keys.map((key) => {
const showNode = Find(nodeInfo, (node) => node.name === key);

View file

@ -4,75 +4,38 @@ import Breadcrumbs from '@material-ui/core/Breadcrumbs';
import classNames from 'classnames';
import Fuse from 'fuse.js';
import Highlighter from 'react-highlight-words';
import isEmpty from 'lodash/isEmpty';
import Link from '@material-ui/core/Link';
import map from 'lodash/map';
import moment from 'moment';
import OrderBy from 'lodash/orderBy';
import Reduce from 'lodash/reduce';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import { Block } from 'shared/Block';
import { Pager } from 'shared/Pager';
import { UsageBreakdown } from '../UsageBreakdown';
import css from './MoleculaTable.module.scss';
type MoleculaTableProps = {
table: any;
dataDistribution: any;
lastUpdated: string;
};
export const MoleculaTable: FC<MoleculaTableProps> = ({
table,
dataDistribution,
lastUpdated
lastUpdated,
}) => {
const [page, setPage] = useState<number>(1);
const [resultsPerPage, setResultsPerPage] = useState<number>(10);
const sliceStart = (page - 1) * resultsPerPage;
const [searchText, setSearchText] = useState<string>('');
const [filteredFields, setFiltereedFields] = useState(table.fields);
const [fieldsData, setFieldsData] = useState<{}>({});
const [maxFieldSize, setMaxFieldSize] = useState<number>(0);
const [fieldsData] = useState<{}>({});
const [sort, setSort] = useState<string>('total');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
const lastUpdatedMoment = lastUpdated ? moment(lastUpdated).utc() : undefined;
useEffect(() => {
if (dataDistribution && !dataDistribution.uncached) {
const aggregatedFieldsData = Reduce(
dataDistribution.fields,
(result, value) => {
let newResult = {};
const keys = Object.keys(value);
keys.forEach(
(key) =>
(newResult[key] = {
total: result[key].total + value[key].total,
fragments: result[key].fragments + value[key].fragments,
keys: result[key].keys + value[key].keys,
metadata: result[key].metadata + value[key].metadata
})
);
return newResult;
}
);
const sorted = OrderBy(aggregatedFieldsData, ['total'], ['desc']);
if (sorted.length > 0) {
setMaxFieldSize(sorted[0].total);
}
setFieldsData(aggregatedFieldsData);
}
}, [dataDistribution]);
useEffect(() => {
if (searchText.length > 1) {
@ -80,7 +43,7 @@ export const MoleculaTable: FC<MoleculaTableProps> = ({
keys: ['name'],
minMatchCharLength: 2,
ignoreLocation: true,
threshold: 0
threshold: 0,
});
const result = fuse.search(searchText);
@ -131,46 +94,6 @@ export const MoleculaTable: FC<MoleculaTableProps> = ({
<Typography variant="h5" color="textSecondary">
{table.name}
</Typography>
{lastUpdatedMoment ? (
<div className={css.infoMessage}>
{dataDistribution && dataDistribution.uncached ? (
<Fragment>
Disk usage will be calculated at the next{` `}
<Tooltip
title={
<Fragment>
Disk and memory information shown here are read from a
cache, the behavior of which can be controlled with the{` `}
<code style={{ whiteSpace: 'nowrap' }}>
--usage-duty-cycle
</code>{' '}
command line flag.
</Fragment>
}
placement="top"
arrow
>
<span className={css.infoTooltip}>cache refresh</span>
</Tooltip>
.
</Fragment>
) : (
<Fragment>
Disk usage last updated{' '}
<Tooltip
title={`${lastUpdatedMoment.format('M/D/YYYY hh:mm a')} UTC`}
placement="top"
arrow
>
<span className={css.infoTooltip}>
{lastUpdatedMoment.fromNow()}
</span>
</Tooltip>
.
</Fragment>
)}
</div>
) : null}
<div className={css.layout}>
<div>
<label className={css.label}>keys</label>
@ -180,9 +103,6 @@ export const MoleculaTable: FC<MoleculaTableProps> = ({
</code>
</div>
</div>
<div className={css.breakdown}>
<UsageBreakdown data={dataDistribution} />
</div>
</div>
<div>
<div>
@ -211,14 +131,14 @@ export const MoleculaTable: FC<MoleculaTableProps> = ({
<TableCell className={css.tableHeader}>
<span
className={classNames(css.sortable, {
[css.currentSort]: sort === 'name'
[css.currentSort]: sort === 'name',
})}
onClick={() => onSortClick('name')}
>
Name{' '}
<ArrowDropDownIcon
className={classNames(css.sortArrow, {
[css.asc]: sortDir === 'asc'
[css.asc]: sortDir === 'asc',
})}
/>
</span>
@ -226,21 +146,6 @@ export const MoleculaTable: FC<MoleculaTableProps> = ({
<TableCell className={css.tableHeader}>Type</TableCell>
<TableCell className={css.tableHeader}>Cardinality</TableCell>
<TableCell className={css.tableHeader}>Options</TableCell>
<TableCell className={css.tableHeader}>
<span
className={classNames(css.sortable, {
[css.currentSort]: sort === 'total'
})}
onClick={() => onSortClick('total')}
>
Disk Usage{' '}
<ArrowDropDownIcon
className={classNames(css.sortArrow, {
[css.asc]: sortDir === 'asc'
})}
/>
</span>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
@ -295,22 +200,6 @@ export const MoleculaTable: FC<MoleculaTableProps> = ({
})}
</div>
</TableCell>
<TableCell className={css.tableCell}>
<UsageBreakdown
data={
isEmpty(field)
? field
: dataDistribution
? dataDistribution.uncached
? dataDistribution
: field
: field
}
width={`${(field.total / maxFieldSize) * 150}px`}
showLabel={false}
usageValueSize="small"
/>
</TableCell>
</TableRow>
);
})}

View file

@ -8,42 +8,29 @@ import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import { Block } from 'shared/Block';
import { SortBy } from 'shared/SortBy';
import { UsageBreakdown } from './UsageBreakdown';
import { useHistory } from 'react-router-dom';
import css from './MoleculaTables.module.scss';
type MoleculaTablesProps = {
tables: any;
dataDistribution: any;
lastUpdated: string;
maxSize: number;
};
export const MoleculaTables: FC<MoleculaTablesProps> = ({
tables,
dataDistribution,
lastUpdated,
maxSize
maxSize,
}) => {
const history = useHistory();
const [sortedTables, setSortedTables] = useState<any>([]);
const lastUpdatedMoment = lastUpdated ? moment(lastUpdated).utc() : undefined;
useEffect(() => {
if (tables && dataDistribution) {
let aggregatedData: any[] = [];
tables.forEach((i) =>
aggregatedData.push({
...dataDistribution[i.name],
...i
})
);
setSortedTables(aggregatedData);
} else if (tables) {
if (tables) {
setSortedTables(tables);
}
}, [tables, dataDistribution]);
}, [tables]);
const handleSortChange = (value: any) => {
const sortDirection = value === 'name' ? 'asc' : 'desc';
@ -96,7 +83,7 @@ export const MoleculaTables: FC<MoleculaTablesProps> = ({
{ label: 'Index Keys Size', value: 'indexKeys' },
{ label: 'Fragment Size', value: 'fragments' },
{ label: 'Field Keys Size', value: 'fieldKeysTotal' },
{ label: 'Metadata Size', value: 'metadata' }
{ label: 'Metadata Size', value: 'metadata' },
]}
defaultValue="name"
onChange={handleSortChange}
@ -111,22 +98,6 @@ export const MoleculaTables: FC<MoleculaTablesProps> = ({
<Card key={name} className={css.tableTile}>
<CardContent>
<div className={css.header}>{name}</div>
<div className={css.section}>
<UsageBreakdown
data={
dataDistribution
? dataDistribution[name]
? dataDistribution[name]
: { uncached: true }
: undefined
}
width={
dataDistribution && dataDistribution[name]
? `${(dataDistribution[name].total / maxSize) * 100}%`
: '0px'
}
/>
</div>
<label className={css.label}>Options</label>
<div className={css.cell}>
<span className={css.label}>keys</span>

View file

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import OrderBy from 'lodash/orderBy';
import { MoleculaTable } from './MoleculaTable';
import { MoleculaTables } from './MoleculaTables';
import { pilosa } from 'services/eventServices';
@ -12,9 +11,8 @@ export const MoleculaTablesContainer = () => {
const history = useHistory();
const [tables, setTables] = useState<any>();
const [selectedTable, setSelectedTable] = useState<any>();
const [dataDistribution, setDataDistribution] = useState<any>();
const [maxSize, setMaxSize] = useState<number>(0);
const [lastUpdated, setLastUpdated] = useState<string>('');
const [maxSize] = useState<number>(0);
const [lastUpdated] = useState<string>('');
useEffectOnce(() => {
pilosa.get
@ -26,48 +24,6 @@ export const MoleculaTablesContainer = () => {
.then((res) => setTables(res.data.indexes))
.catch((err) => console.log(err))
);
pilosa.get.usage().then((res) => {
const nodes = Object.keys(res.data);
let data = {};
nodes.forEach((node) => {
const nodeIndexes = res.data[node].diskUsage.indexes;
const indexList = Object.keys(nodeIndexes);
indexList.forEach((i) => {
const nodeData = nodeIndexes[i];
if (data[i]) {
data[i] = {
total: data[i].total + nodeData.total,
fieldKeysTotal: data[i].fieldKeysTotal + nodeData.fieldKeysTotal,
indexKeys: data[i].indexKeys + nodeData.indexKeys,
fragments: data[i].fragments + nodeData.fragments,
metadata: data[i].metadata + nodeData.metadata,
fields: [...data[i].fields, nodeData.fields]
};
} else {
data[i] = {
total: nodeData.total,
fieldKeysTotal: nodeData.fieldKeysTotal,
indexKeys: nodeData.indexKeys,
fragments: nodeData.fragments,
metadata: nodeData.metadata,
fields: [nodeData.fields]
};
}
});
if(!lastUpdated) {
setLastUpdated(res.data[node].lastUpdated);
}
});
const sorted = OrderBy(data, ['total'], ['desc']);
if (sorted.length > 0) {
setMaxSize(sorted[0].total);
}
setDataDistribution(data);
});
});
useEffect(() => {
@ -85,21 +41,10 @@ export const MoleculaTablesContainer = () => {
}, [match, tables, history]);
return selectedTable ? (
<MoleculaTable
table={selectedTable}
dataDistribution={
dataDistribution
? dataDistribution[selectedTable.name]
? dataDistribution[selectedTable.name]
: { uncached: true }
: undefined
}
lastUpdated={lastUpdated}
/>
<MoleculaTable table={selectedTable} lastUpdated={lastUpdated} />
) : (
<MoleculaTables
tables={tables}
dataDistribution={dataDistribution}
lastUpdated={lastUpdated}
maxSize={maxSize}
/>

View file

@ -1,62 +0,0 @@
.label {
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 4px;
font-weight: 400;
}
.usageBreakdown {
display: flex;
align-items: center;
.usageBreakdownLabel {
white-space: nowrap;
margin-right: 8px;
&.smallLabel {
font-size: 12px;
}
}
}
.breakdown {
display: flex;
align-items: center;
height: 13px;
border-radius: 4px;
background: rgba(var(--contrast-rgb), 0.1);
.fieldKeysTotal {
height: 13px;
background: rgba(88, 80, 141, 0.7);
}
.indexKeys {
height: 13px;
background: rgba(255, 99, 97, 0.7);
}
.keys {
height: 13px;
background: rgba(88, 80, 141, 0.7);
}
.fragments {
height: 13px;
background: rgba(255, 166, 0, 0.7);
}
.metadata {
height: 13px;
background: rgba(188, 80, 144, 0.7);
}
.bar:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.bar:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}

View file

@ -1,183 +0,0 @@
import React, { FC, Fragment } from 'react';
import classNames from 'classnames';
import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import { formatBytes } from 'shared/utils/formatBytes';
import css from './UsageBreakdown.module.scss';
type UsageBreakdownProps = {
data: any;
width?: string;
showLabel?: boolean;
usageValueSize?: 'small' | 'medium';
};
export const UsageBreakdown: FC<UsageBreakdownProps> = ({
data = {},
width,
showLabel = true,
usageValueSize = 'medium'
}) => {
const {
total,
fieldKeysTotal,
indexKeys,
fragments,
metadata,
keys,
uncached
} = data;
const fieldKeysPercentage =
fieldKeysTotal && total ? (fieldKeysTotal / total) * 100 : 0;
const indexKeysPercentage = indexKeys ? (indexKeys / total) * 100 : 0;
const fragmentsPercentage = fragments ? (fragments / total) * 100 : 0;
const metadataPercentage = metadata ? (metadata / total) * 100 : 0;
const keysPercentage = keys && total ? (keys / total) * 100 : 0;
return (
<Fragment>
{showLabel ? <label className={css.label}>Data</label> : null}
<div className={css.usageBreakdown}>
{total ? (
<Fragment>
<span
className={classNames(css.usageBreakdownLabel, {
[css.smallLabel]: usageValueSize === 'small'
})}
>
{formatBytes(total)}
</span>
<div
className={css.breakdown}
style={{ width: width ? width : '100%' }}
>
{fieldKeysTotal ? (
<Tooltip
title={
<Fragment>
<label>Field Keys:</label>
<Typography variant="caption" component="div">
{formatBytes(fieldKeysTotal)} (
{fieldKeysPercentage.toLocaleString(undefined, {
maximumFractionDigits: 1
})}
%)
</Typography>
</Fragment>
}
placement="top"
arrow
>
<div
className={classNames(css.bar, css.fieldKeysTotal)}
style={{ width: `${fieldKeysPercentage}%` }}
/>
</Tooltip>
) : null}
{indexKeys ? (
<Tooltip
title={
<Fragment>
<label>Index Keys:</label>
<Typography variant="caption" component="div">
{formatBytes(indexKeys)} (
{indexKeysPercentage.toLocaleString(undefined, {
maximumFractionDigits: 1
})}
%)
</Typography>
</Fragment>
}
placement="top"
arrow
>
<div
className={classNames(css.bar, css.indexKeys)}
style={{ width: `${indexKeysPercentage}%` }}
/>
</Tooltip>
) : null}
{keys ? (
<Tooltip
title={
<Fragment>
<label>Keys:</label>
<Typography variant="caption" component="div">
{formatBytes(keys)} (
{keysPercentage.toLocaleString(undefined, {
maximumFractionDigits: 1
})}
%)
</Typography>
</Fragment>
}
placement="top"
arrow
>
<div
className={classNames(css.bar, css.keys)}
style={{ width: `${keysPercentage}%` }}
/>
</Tooltip>
) : null}
{fragments ? (
<Tooltip
title={
<Fragment>
<label>Fragments:</label>
<Typography variant="caption" component="div">
{formatBytes(fragments)} (
{fragmentsPercentage.toLocaleString(undefined, {
maximumFractionDigits: 1
})}
%)
</Typography>
</Fragment>
}
placement="top"
arrow
>
<div
className={classNames(css.bar, css.fragments)}
style={{ width: `${fragmentsPercentage}%` }}
/>
</Tooltip>
) : null}
{metadata ? (
<Tooltip
title={
<Fragment>
<label>Metadata:</label>
<Typography variant="caption" component="div">
{formatBytes(metadata)} (
{metadataPercentage.toLocaleString(undefined, {
maximumFractionDigits: 1
})}
%)
</Typography>
</Fragment>
}
placement="top"
arrow
>
<div
className={classNames(css.bar, css.metadata)}
style={{ width: `${metadataPercentage}%` }}
/>
</Tooltip>
) : null}
</div>
</Fragment>
) : uncached ? (
<Typography variant="caption" component="div">
Waiting...
</Typography>
) : (
<Typography variant="caption" component="div">
Calculating...
</Typography>
)}
</div>
</Fragment>
);
};

View file

@ -1 +0,0 @@
export * from './UsageBreakdown';

View file

@ -42,9 +42,6 @@ export const pilosa = {
metrics() {
return api.get('/metrics.json');
},
usage() {
return api.get('/ui/usage');
},
queryHistory() {
return api.get('/query-history');
},

View file

@ -214,9 +214,6 @@ type Config struct {
// LookupDBDSN is an external database to connect to for `ExternalLookup` queries.
LookupDBDSN string `toml:"lookup-db-dsn"`
// The percentage of time spent recalculating the disk and memory usage cache.
UsageDutyCycle float64 `toml:"usage-duty-cycle"`
// Future flags are used to represent features or functionality which is not
// yet the default behavior, but will be in a future release.
Future struct {
@ -390,9 +387,6 @@ func NewConfig() *Config {
c.Etcd.PeerCertFile = ""
c.Etcd.PeerKeyFile = ""
// Disk and Memory Usage
c.UsageDutyCycle = 20.0
// Future flags.
c.Future.Rename = false

View file

@ -517,48 +517,6 @@ func TestHandler_Endpoints(t *testing.T) {
}
})
// UI/usage returns disk and memory usage from a precalculated cache.
// Since the cache calculates the cache on server startup, and tests create indexes thereafter
// the cache initially has 0 indexes when the test suite is ran. Therefore, this test first
// resets the cache.
t.Run("UI/usage", func(t *testing.T) {
if cmd.API.ResetUsageCache() != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/ui/usage", nil))
if w.Code != gohttp.StatusOK {
t.Fatalf("unexpected status code: %d", w.Code)
}
nodeUsages := make(map[string]pilosa.NodeUsage)
if err := json.Unmarshal(w.Body.Bytes(), &nodeUsages); err != nil {
t.Fatalf("unmarshal")
}
for _, nodeUsage := range nodeUsages {
if nodeUsage.Disk.TotalUse < 1 {
t.Fatalf("expected some disk use, got %d", nodeUsage.Disk.TotalUse)
}
if nodeUsage.Disk.Capacity < 1 {
t.Fatalf("expected some disk capacity, got %d", nodeUsage.Disk.Capacity)
}
if nodeUsage.Memory.TotalUse < 1 {
t.Fatalf("expected some memory use, got %d", nodeUsage.Memory.TotalUse)
}
if nodeUsage.Memory.Capacity < 1 {
t.Fatalf("expected some memory capacity, got %d", nodeUsage.Memory.Capacity)
}
numIndexes := len(nodeUsage.Disk.IndexUsage)
if numIndexes != 3 {
t.Fatalf("wrong length index usage list: expected %d, got %d", 3, numIndexes)
}
numFields := len(nodeUsage.Disk.IndexUsage["i1"].Fields)
if numFields != len(i1.Fields()) {
t.Fatalf("wrong length field usage list: expected %d, got %d", len(i1.Fields()), numFields)
}
}
})
t.Run("UI/shard-distribution", func(t *testing.T) {
// This tests the response structure, not the cluster behavior.
w := httptest.NewRecorder()

View file

@ -271,8 +271,6 @@ func (m *Command) Start() (err error) {
}
}
go m.API.RefreshUsageCache(m.Config.UsageDutyCycle)
_ = testhook.Opened(pilosa.NewAuditor(), m, nil)
close(m.Started)
return nil

View file

@ -4,7 +4,6 @@ package pilosa
import (
"fmt"
"os"
"path"
"strings"
"sync"
@ -471,192 +470,6 @@ func (f *TxFactory) DeleteFragmentFromStore(
return f.dbPerShard.DeleteFragment(index, field, view, shard, frag)
}
// IndexUsageDetails computes the sum of filesizes used by the node, broken down
// by index, field, fragments and keys.
func (f *TxFactory) IndexUsageDetails(isClosing func() bool) (map[string]IndexUsage, uint64, error) {
indexUsage := make(map[string]IndexUsage)
holderPath, err := expandDirName(f.holder.path)
if err != nil {
return indexUsage, 0, errors.Wrap(err, "expanding data directory")
}
indexesPath, err := expandDirName(f.holder.IndexesPath())
if err != nil {
return indexUsage, 0, errors.Wrap(err, "expanding indexes directory")
}
idxs := f.holder.Indexes()
qcx := f.NewQcx()
defer qcx.Abort()
for _, idx := range idxs {
index := idx.name
indexPath := path.Join(indexesPath, index)
// field usage
fieldUsages := make(map[string]FieldUsage)
fragmentsTotal := uint64(0)
fieldKeysTotal := uint64(0)
fieldMetaBytesTotal := uint64(0)
fieldsTotal := uint64(0)
flds := idx.Fields()
for _, fld := range flds {
field := fld.Name()
if field == "_keys" {
continue
}
fUsage, err := f.fieldUsage(indexPath, fld)
if err != nil {
return indexUsage, 0, errors.Wrapf(err, "getting disk usage for index (%s)", index)
}
// non-roaring field usage
fragmentUsage := uint64(0)
for _, shard := range fld.AvailableShards(true).Slice() {
if isClosing() {
return nil, 0, nil
}
if err := func() error {
tx, finisher, err := qcx.GetTx(Txo{Write: !writable, Index: idx, Shard: shard})
if err != nil {
return errors.Wrap(err, "qcx.GetTx")
}
defer finisher(nil)
fieldBytes, err := tx.GetFieldSizeBytes(index, field)
if err != nil {
return errors.Wrapf(err, "getting disk usage for non-roaring fragments (%s)", field)
}
fragmentUsage += fieldBytes
return nil
}(); err != nil {
return indexUsage, 0, err
}
}
// add non-roaring to roaring
fUsage.Fragments += fragmentUsage
fUsage.Total += fragmentUsage
// add to running total
fieldMetaBytesTotal += fUsage.Metadata
fieldKeysTotal += fUsage.Keys
fragmentsTotal += fUsage.Fragments
fieldsTotal += fUsage.Total
fieldUsages[field] = fUsage
}
// index metadata
indexMetaBytes, err := directoryUsage(indexPath, false)
if err != nil {
return indexUsage, 0, errors.Wrapf(err, "getting disk usage for index metadata (%s)", index)
}
// index keys usage
indexKeysBytes := uint64(0)
if idx.keys {
keysPath := path.Join(indexPath, translateStoreDir)
indexKeysBytes, _ = directoryUsage(keysPath, true) // if directory doesn't exist, size = 0
}
indexUsage[index] = IndexUsage{
Total: indexMetaBytes + indexKeysBytes + fieldsTotal,
Metadata: indexMetaBytes + fieldMetaBytesTotal,
IndexKeys: indexKeysBytes,
FieldKeysTotal: fieldKeysTotal,
Fragments: fragmentsTotal,
Fields: fieldUsages,
}
}
// node metadata, e.g. id allocator
nodeMetaBytes, err := directoryUsage(holderPath, false)
if err != nil {
return indexUsage, 0, errors.Wrapf(err, "getting disk usage for node metadata")
}
return indexUsage, nodeMetaBytes, nil
}
// fieldUsage computes the sum of filesizes used by a field in
// the filesystem tree (roaring storage), broken down by keys and fragments.
func (f *TxFactory) fieldUsage(indexPath string, fld *Field) (FieldUsage, error) {
fieldUsage := FieldUsage{}
field := fld.name
// row keys
keysBytes := int64(0)
var err error
keysBytes, err = fileSize(fld.TranslateStorePath())
if err != nil {
// if file doesn't exist, size = 0
keysBytes = 0
}
// field metadata
fieldPath := path.Join(indexPath, FieldsDir, field)
metaBytes, err := directoryUsage(fieldPath, false) // this includes keys
if err != nil {
return fieldUsage, errors.Wrapf(err, "getting disk usage for field meta (%s)", field)
}
// fragment data
viewsPath := path.Join(fieldPath, "views")
fragmentBytes := uint64(0)
if dirExists(viewsPath) {
fragmentBytes, err = directoryUsage(viewsPath, true)
if err != nil {
return fieldUsage, errors.Wrapf(err, "getting disk usage for field fragments (%s)", field)
}
}
fieldUsage = FieldUsage{
Total: metaBytes + fragmentBytes, // metaBytes includes keys
Metadata: metaBytes - uint64(keysBytes),
Fragments: fragmentBytes,
Keys: uint64(keysBytes),
}
return fieldUsage, nil
}
// NOTE: Go 1.16 introduced a new Readdir() method that is supposed to be more performant.
// Not yet upgraded b/c new method is not compatible with older versions of Go.
func directoryUsage(fname string, recursive bool) (uint64, error) {
if !dirExists(fname) {
return 0, errors.Errorf("directory does not exist (%s)", fname)
}
var size uint64
dir, err := os.Open(fname)
if err != nil {
return 0, errors.Wrap(err, "opening data subdirectory")
}
defer dir.Close()
files, err := dir.Readdir(-1)
if err != nil {
return 0, errors.Wrap(err, "reading data subdirectory")
}
for _, file := range files {
if recursive && file.IsDir() {
sz, err := directoryUsage(path.Join(fname, file.Name()), true)
if err != nil {
return 0, err
}
size += sz
} else {
size += uint64(file.Size()) // NOTE this cast is safe for regular files, not necessarily others
}
}
return size, nil
}
// CloseIndex is a no-op. This seems to be in place for debugging purposes.
func (f *TxFactory) CloseIndex(idx *Index) error {
return nil