Repository: threefoldtech/zos
Pull request #1621 (2022-03-14 14:54:35)

Set used capacity on chain by the node


Patch:
diff --git a/cmds/modules/provisiond/cap.go b/cmds/modules/provisiond/cap.go
new file mode 100644
index 00000000..61b3a94d
--- /dev/null
+++ b/cmds/modules/provisiond/cap.go
@@ -0,0 +1,114 @@
+package provisiond
+
+import (
+	"context"
+
+	"github.com/centrifuge/go-substrate-rpc-client/v4/types"
+	"github.com/rs/zerolog/log"
+	"github.com/threefoldtech/substrate-client"
+	"github.com/threefoldtech/zos/pkg/gridtypes"
+	"github.com/threefoldtech/zos/pkg/provision"
+)
+
+type DeploymentID struct {
+	Twin     uint32
+	Contract uint64
+}
+
+type CapacitySetter struct {
+	id      substrate.Identity
+	sub     substrate.Manager
+	ch      chan DeploymentID
+	storage provision.Storage
+}
+
+func NewCapacitySetter(id substrate.Identity, sub substrate.Manager, storage provision.Storage) CapacitySetter {
+	return CapacitySetter{
+		id:      id,
+		sub:     sub,
+		storage: storage,
+		ch:      make(chan DeploymentID, 215),
+	}
+}
+
+func (c *CapacitySetter) Callback(twin uint32, contract uint64, delete bool) {
+	// we don't set capacity on the grid on deletion
+	if delete {
+		return
+	}
+
+	// we just push it to channel so we can return as soon
+	// as possible. The channel should have enough capacity
+	// to accept enough active contracts.
+	c.ch <- DeploymentID{Twin: twin, Contract: contract}
+}
+
+func (c *CapacitySetter) setWithClient(cl *substrate.Substrate, deployments ...gridtypes.Deployment) error {
+	caps := make([]substrate.ContractResources, 0, len(deployments))
+	for _, deployment := range deployments {
+		var total gridtypes.Capacity
+		for i := range deployment.Workloads {
+			wl := &deployment.Workloads[i]
+			if wl.Result.State.IsAny(gridtypes.StateOk) {
+				cap, err := wl.Capacity()
+				if err != nil {
+					log.Error().Err(err).Str("workload", wl.Name.String()).
+						Msg("failed to compute capacity consumption for workload")
+					continue
+				}
+
+				total.Add(&cap)
+			}
+		}
+		caps = append(caps, substrate.ContractResources{
+			ContractID: types.U64(deployment.ContractID),
+			Used: substrate.Resources{
+				HRU: types.U64(total.HRU),
+				SRU: types.U64(total.SRU),
+				CRU: types.U64(total.CRU),
+				MRU: types.U64(total.MRU),
+			},
+		})
+	}
+
+	// TODO: retry ?
+	return cl.SetContractConsumption(c.id, caps...)
+}
+
+func (c *CapacitySetter) Set(deployment ...gridtypes.Deployment) error {
+	if len(deployment) == 0 {
+		return nil
+	}
+
+	cl, err := c.sub.Substrate()
+	if err != nil {
+		return err
+	}
+
+	defer cl.Close()
+
+	return c.setWithClient(cl, deployment...)
+}
+
+func (c *CapacitySetter) Run(ctx context.Context) error {
+	for {
+		var id DeploymentID
+		select {
+		case <-ctx.Done():
+			return nil
+		case id = <-c.ch:
+		}
+
+		log := log.With().Uint32("twin", id.Twin).Uint64("contract", id.Contract).Logger()
+
+		deployment, err := c.storage.Get(id.Twin, id.Contract)
+		if err != nil {
+			log.Error().Err(err).Msg("failed to get deployment")
+			continue
+		}
+
+		if err := c.Set(deployment); err != nil {
+			log.Error().Err(err).Msg("failed to set contract usage")
+		}
+	}
+}
diff --git a/cmds/modules/provisiond/main.go b/cmds/modules/provisiond/main.go
index 3d24f50f..ca0f8049 100644
--- a/cmds/modules/provisiond/main.go
+++ b/cmds/modules/provisiond/main.go
@@ -195,6 +195,7 @@ func action(cli *cli.Context) error {
 		return errors.Wrap(err, "failed to get node reserved capacity")
 	}
 	var current gridtypes.Capacity
+	var active []gridtypes.Deployment
 	if !app.IsFirstBoot(serverName) {
 		// if this is the first boot of this module.
 		// it means the provision engine will still
@@ -203,7 +204,7 @@ func action(cli *cli.Context) error {
 		// since the counters will get populated anyway.
 		// but if not, we need to set the current counters
 		// from store.
-		current, err = store.Capacity()
+		current, active, err = store.Capacity()
 		if err != nil {
 			log.Error().Err(err).Msg("failed to compute current consumed capacity")
 		}
@@ -264,6 +265,21 @@ func action(cli *cli.Context) error {
 		return errors.Wrap(err, "failed to create storage for queues")
 	}
 
+	setter := NewCapacitySetter(kp, mgr, store)
+
+	log.Info().Int("contracts", len(active)).Msg("setting used capacity by contracts")
+	if err := setter.Set(active...); err != nil {
+		log.Error().Err(err).Msg("failed to set capacity for active contracts")
+	}
+
+	log.Info().Msg("setting contracts used cpacity done")
+
+	go func() {
+		if err := setter.Run(ctx); err != nil {
+			log.Fatal().Err(err).Msg("capacity reporter exited unexpectedly")
+		}
+	}()
+
 	engine, err := provision.New(
 		store,
 		statistics,
@@ -284,6 +300,10 @@ func action(cli *cli.Context) error {
 		// if this is a node reboot, the node needs to
 		// recreate all reservations. so we set rerun = true
 		provision.WithRerunAll(app.IsFirstBoot(serverName)),
+		// Callback when a deployment changes capacity it must
+		// be called. this one used by the setter to set used
+		// capacity on chain.
+		provision.WithCallback(setter.Callback),
 	)
 
 	if err != nil {
@@ -337,7 +357,7 @@ func action(cli *cli.Context) error {
 	go func() {
 		for {
 			err := reporter.Run(ctx)
-			if err != context.Canceled {
+			if err == context.Canceled {
 				return
 			} else if err != nil {
 				log.Error().Err(err).Msg("capacity reported stopped unexpectedely")
diff --git a/go.mod b/go.mod
index f4a6337c..164eb2d6 100644
--- a/go.mod
+++ b/go.mod
@@ -62,7 +62,7 @@ require (
 	github.com/stretchr/testify v1.7.0
 	github.com/threefoldtech/0-fs v1.3.1-0.20201203163303-d963de9adea7
 	github.com/threefoldtech/go-rmb v0.1.11-0.20220224131627-825c23c921d3
-	github.com/threefoldtech/substrate-client v0.0.0-20220311155549-b19ac3fcada8
+	github.com/threefoldtech/substrate-client v0.0.0-20220314145040-31642882d426
 	github.com/threefoldtech/zbus v0.1.5
 	github.com/tinylib/msgp v1.1.5 // indirect
 	github.com/tyler-smith/go-bip39 v1.1.0
diff --git a/go.sum b/go.sum
index 7cf2e6c2..d08d1fd1 100644
--- a/go.sum
+++ b/go.sum
@@ -1072,6 +1072,8 @@ github.com/threefoldtech/substrate-client v0.0.0-20220308140931-04dc2e0dad45 h1:
 github.com/threefoldtech/substrate-client v0.0.0-20220308140931-04dc2e0dad45/go.mod h1:Lpsz7gVNSdbWD8erfVh1AZv2wnProc7g/QHJiYp5lP8=
 github.com/threefoldtech/substrate-client v0.0.0-20220311155549-b19ac3fcada8 h1:uYfTsfBbk4DIsEmM6dKlqQC/a8HpN+aECK61uiddrFQ=
 github.com/threefoldtech/substrate-client v0.0.0-20220311155549-b19ac3fcada8/go.mod h1:Lpsz7gVNSdbWD8erfVh1AZv2wnProc7g/QHJiYp5lP8=
+github.com/threefoldtech/substrate-client v0.0.0-20220314145040-31642882d426 h1:mFEPNaYKvaQtFZ+6l7S00gl1AVBj96T7/VJwCU7HFUY=
+github.com/threefoldtech/substrate-client v0.0.0-20220314145040-31642882d426/go.mod h1:Lpsz7gVNSdbWD8erfVh1AZv2wnProc7g/QHJiYp5lP8=
 github.com/threefoldtech/zbus v0.1.5 h1:S9kXbjejoRRnJw1yKHEXFGF2vqL+Drae2n4vpj0pGHo=
 github.com/threefoldtech/zbus v0.1.5/go.mod h1:ZtiRpcqzEBJetVQDsEbw0p48h/AF3O1kf0tvd30I0BU=
 github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
diff --git a/pkg/gridtypes/deployment.go b/pkg/gridtypes/deployment.go
index 3d455cf9..1b53a38f 100644
--- a/pkg/gridtypes/deployment.go
+++ b/pkg/gridtypes/deployment.go
@@ -122,6 +122,21 @@ type Deployment struct {
 	Workloads []Workload `json:"workloads"`
 }
 
+// IsActive return true if the deployment has
+// workloads in deployable state
+func (d *Deployment) IsActive() bool {
+	active := false
+	for i := range d.Workloads {
+		wl := &d.Workloads[i]
+		if !wl.Result.State.IsAny(StateDeleted, StateError) {
+			// not delete or error so is probably active
+			return true
+		}
+	}
+
+	return active
+}
+
 // SetError sets an error on ALL workloads. this is mostly
 // an error caused by validation AFTTER the deployment was initially accepted
 func (d *Deployment) SetError(err error) {
diff --git a/pkg/provision/engine.go b/pkg/provision/engine.go
index cfdd66c5..2c0318b9 100644
--- a/pkg/provision/engine.go
+++ b/pkg/provision/engine.go
@@ -55,12 +55,32 @@ func WithRerunAll(t bool) EngineOption {
 	return &withRerunAll{t}
 }
 
+type Callback func(twin uint32, contract uint64, delete bool)
+
+// WithCallback sets a callback that is called when a deployment is being Created, Updated, Or Deleted
+// The handler then can use the id to get current "state" of the deployment from storage and
+// take proper action. A callback must not block otherwise the engine operation will get blocked
+func WithCallback(cb Callback) EngineOption {
+	return &withCallback{cb}
+}
+
 type jobOperation int
 
 const (
+	// opProvision installs a deployment
 	opProvision jobOperation = iota
+	// removes a deployment
 	opDeprovision
+	// deletes a deployment
 	opUpdate
+	// opProvisionNoContract is used to reinstall
+	// a deployment on node reboot without validating
+	// against the chain again because 1) validation
+	// has already been done on first installation
+	// 2) hash is not granteed to match because of the
+	// order of the workloads doesn't have to match
+	// the one sent by the user
+	opProvisionNoContract
 )
 
 // engineJob is a persisted job instance that is
@@ -91,8 +111,9 @@ type NativeEngine struct {
 	typeIndex map[gridtypes.WorkloadType]int
 	rerunAll  bool
 	//substrate specific attributes
-	nodeID uint32
-	sub    substrate.Manager
+	nodeID   uint32
+	sub      substrate.Manager
+	callback Callback
 }
 
 var _ Engine = (*NativeEngine)(nil)
@@ -159,6 +180,14 @@ func (w *withRerunAll) apply(e *NativeEngine) {
 	e.rerunAll = w.t
 }
 
+type withCallback struct {
+	cb Callback
+}
+
+func (w *withCallback) apply(e *NativeEngine) {
+	e.callback = w.cb
+}
+
 type nullKeyGetter struct{}
 
 func (n *nullKeyGetter) GetKey(id uint32) ([]byte, error) {
@@ -397,7 +426,7 @@ func (e *NativeEngine) Run(root context.Context) error {
 
 		// contract validation
 		// this should ONLY be done on provosion and update operation
-		if job.Op != opDeprovision {
+		if job.Op == opProvision || job.Op == opUpdate {
 			// otherwise, contract validation is needed
 			ctx, err = e.contract(ctx, &job.Target)
 			if err != nil {
@@ -415,6 +444,8 @@ func (e *NativeEngine) Run(root context.Context) error {
 		}
 
 		switch job.Op {
+		case opProvisionNoContract:
+			fallthrough
 		case opProvision:
 			e.installDeployment(ctx, &job.Target)
 		case opDeprovision:
@@ -444,9 +475,25 @@ func (e *NativeEngine) Run(root context.Context) error {
 		if err != nil {
 			log.Error().Err(err).Msg("failed to dequeue job")
 		}
+
+		e.safeCallback(&job.Target, job.Op == opDeprovision)
 	}
 }
 
+func (e *NativeEngine) safeCallback(d *gridtypes.Deployment, delete bool) {
+	if e.callback == nil {
+		return
+	}
+	// in case callback panics we don't want to kill the engine
+	defer func() {
+		if err := recover(); err != nil {
+			log.Error().Msgf("panic while processing callback: %v", err)
+		}
+	}()
+
+	e.callback(d.TwinID, d.ContractID, delete)
+}
+
 // contract validates and injects the deployment contracts is substrate is configured
 // for this instance of the provision engine.
 func (e *NativeEngine) contract(ctx context.Context, dl *gridtypes.Deployment) (context.Context, error) {
@@ -511,8 +558,22 @@ func (e *NativeEngine) boot(root context.Context) error {
 			// unfortunately we have to inject this value here
 			// since the boot runs outside the engine queue.
 
-			ctx := withDeployment(root, dl.TwinID, dl.ContractID)
-			e.installDeployment(ctx, &dl)
+			if !dl.IsActive() {
+				continue
+			}
+
+			job := engineJob{
+				Target: dl,
+				Op:     opProvisionNoContract,
+			}
+
+			if err := e.queue.Enqueue(&job); err != nil {
+				log.Error().
+					Err(err).
+					Uint32("twin", dl.TwinID).
+					Uint64("dl", dl.ContractID).
+					Msg("failed to queue deployment for processing")
+			}
 		}
 	}
 
diff --git a/pkg/provision/storage/storage.go b/pkg/provision/storage/storage.go
index 29189739..2407234b 100644
--- a/pkg/provision/storage/storage.go
+++ b/pkg/provision/storage/storage.go
@@ -647,10 +647,10 @@ func (b *BoltStorage) ByTwin(twin uint32) ([]uint64, error) {
 	return deployments, err
 }
 
-func (b *BoltStorage) Capacity() (cap gridtypes.Capacity, err error) {
+func (b *BoltStorage) Capacity() (cap gridtypes.Capacity, active []gridtypes.Deployment, err error) {
 	twins, err := b.Twins()
 	if err != nil {
-		return gridtypes.Capacity{}, err
+		return gridtypes.Capacity{}, nil, err
 	}
 
 	for _, twin := range twins {
@@ -666,6 +666,7 @@ func (b *BoltStorage) Capacity() (cap gridtypes.Capacity, err error) {
 				continue
 			}
 
+			isActive := false
 			for _, wl := range deployment.Workloads {
 				if wl.Result.State != gridtypes.StateOk {
 					continue
@@ -673,15 +674,19 @@ func (b *BoltStorage) Capacity() (cap gridtypes.Capacity, err error) {
 
 				c, err := wl.Capacity()
 				if err != nil {
-					return cap, err
+					return cap, nil, err
 				}
 
+				isActive = true
 				cap.Add(&c)
 			}
+			if isActive {
+				active = append(active, deployment)
+			}
 		}
 	}
 
-	return cap, nil
+	return cap, active, nil
 }
 
 func (b *BoltStorage) Close() error {
diff --git a/pkg/rrd/rrd.go b/pkg/rrd/rrd.go
index 89bd7771..f9d3f4f7 100644
--- a/pkg/rrd/rrd.go
+++ b/pkg/rrd/rrd.go
@@ -136,7 +136,7 @@ func (r *rrdBolt) Counters(since time.Time) (map[string]float64, error) {
 	ts = (ts / r.window) * r.window
 
 	// we start from the previous slot so we check from the last value.
-	ts -= r.window
+	//ts -= r.window
 	change := make(map[string]float64)
 
 	err := r.db.View(func(tx *bolt.Tx) error {
diff --git a/tools/certify/main.go b/tools/certify/main.go
index 56464b2f..338f4316 100644
--- a/tools/certify/main.go
+++ b/tools/certify/main.go
@@ -69,7 +69,7 @@ func run(opt options) error {
 		}
 
 		log.Info().Msg("possible node to certify")
-		possible += 1
+		possible++
 		if opt.dry {
 			continue
 		}
@@ -79,7 +79,7 @@ func run(opt options) error {
 			continue
 		}
 
-		certified += 1
+		certified++
 	}
 
 	log.Info().Int("count", possible).Msg("found nodes that can be certified")
diff --git a/tools/storagesize/main.go b/tools/storagesize/main.go
deleted file mode 100644
index ee4c2a9f..00000000
--- a/tools/storagesize/main.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package main
-
-import (
-	"fmt"
-
-	"github.com/threefoldtech/zos/pkg/gridtypes/zos"
-	"github.com/threefoldtech/zos/pkg/storage"
-)
-
-func main() {
-	s, err := storage.New()
-	if err != nil {
-		panic(fmt.Sprintf("%v", err))
-	}
-
-	kind := zos.SSDDevice
-	total, err := s.Total(kind)
-	if err != nil {
-		panic(fmt.Sprintf("%v", err))
-	}
-
-	fmt.Printf("SSD: %v\n", total)
-
-	kind = zos.HDDDevice
-	total, err = s.Total(kind)
-	if err != nil {
-		panic(fmt.Sprintf("%v", err))
-	}
-
-	fmt.Printf("HDD: %v\n", total)
-}
diff --git a/tools/subproxy/.gitignore b/tools/subproxy/.gitignore
deleted file mode 100644
index c87131a9..00000000
--- a/tools/subproxy/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-subproxy
diff --git a/tools/subproxy/Dockerfile b/tools/subproxy/Dockerfile
deleted file mode 100644
index 4dadcff4..00000000
--- a/tools/subproxy/Dockerfile
+++ /dev/null
@@ -1,9 +0,0 @@
-FROM golang:alpine3.14 as BUILDER
-WORKDIR /opt/zos
-COPY . .
-WORKDIR /opt/zos/tools/subproxy/
-RUN go build
-
-FROM alpine:3.13.5
-COPY --from=BUILDER /opt/zos/tools/subproxy/subproxy /bin/
-ENTRYPOINT [ "/bin/subproxy" ]
\ No newline at end of file
diff --git a/tools/subproxy/chart/.helmignore b/tools/subproxy/chart/.helmignore
deleted file mode 100644
index 0e8a0eb3..00000000
--- a/tools/subproxy/chart/.helmignore
+++ /dev/null
@@ -1,23 +0,0 @@
-# Patterns to ignore when building packages.
-# This supports shell glob matching, relative path matching, and
-# negation (prefixed with !). Only one pattern per line.
-.DS_Store
-# Common VCS dirs
-.git/
-.gitignore
-.bzr/
-.bzrignore
-.hg/
-.hgignore
-.svn/
-# Common backup files
-*.swp
-*.bak
-*.tmp
-*.orig
-*~
-# Various IDEs
-.project
-.idea/
-*.tmproj
-.vscode/
diff --git a/tools/subproxy/chart/Chart.yaml b/tools/subproxy/chart/Chart.yaml
deleted file mode 100644
index 12889177..00000000
--- a/tools/subproxy/chart/Chart.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-apiVersion: v2
-name: subproxy
-description: A Helm chart for Kubernetes
-type: application
-version: 0.1.0
-appVersion: "1.16.0"
diff --git a/tools/subproxy/chart/templates/_helpers.tpl b/tools/subproxy/chart/templates/_helpers.tpl
deleted file mode 100644
index 229f9fb0..00000000
--- a/tools/subproxy/chart/templates/_helpers.tpl
+++ /dev/null
@@ -1,62 +0,0 @@
-{{/*
-Expand the name of the chart.
-*/}}
-{{- define "subproxy.name" -}}
-{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
-{{- end }}
-
-{{/*
-Create a default fully qualified app name.
-We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
-If release name contains chart name it will be used as a full name.
-*/}}
-{{- define "subproxy.fullname" -}}
-{{- if .Values.fullnameOverride }}
-{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
-{{- else }}
-{{- $name := default .Chart.Name .Values.nameOverride }}
-{{- if contains $name .Release.Name }}
-{{- .Release.Name | trunc 63 | trimSuffix "-" }}
-{{- else }}
-{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
-{{- end }}
-{{- end }}
-{{- end }}
-
-{{/*
-Create chart name and version as used by the chart label.
-*/}}
-{{- define "subproxy.chart" -}}
-{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
-{{- end }}
-
-{{/*
-Common labels
-*/}}
-{{- define "subproxy.labels" -}}
-helm.sh/chart: {{ include "subproxy.chart" . }}
-{{ include "subproxy.selectorLabels" . }}
-{{- if .Chart.AppVersion }}
-app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
-{{- end }}
-app.kubernetes.io/managed-by: {{ .Release.Service }}
-{{- end }}
-
-{{/*
-Selector labels
-*/}}
-{{- define "subproxy.selectorLabels" -}}
-app.kubernetes.io/name: {{ include "subproxy.name" . }}
-app.kubernetes.io/instance: {{ .Release.Name }}
-{{- end }}
-
-{{/*
-Create the name of the service account to use
-*/}}
-{{- define "subproxy.serviceAccountName" -}}
-{{- if .Values.serviceAccount.create }}
-{{- default (include "subproxy.fullname" .) .Values.serviceAccount.name }}
-{{- else }}
-{{- default "default" .Values.serviceAccount.name }}
-{{- end }}
-{{- end }}
diff --git a/tools/subproxy/chart/templates/deployment.yaml b/tools/subproxy/chart/templates/deployment.yaml
deleted file mode 100644
index 2063e303..00000000
--- a/tools/subproxy/chart/templates/deployment.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: {{ include "subproxy.fullname" . }}
-  labels:
-    {{- include "subproxy.labels" . | nindent 4 }}
-spec:
-  selector:
-    matchLabels:
-      {{- include "subproxy.selectorLabels" . | nindent 6 }}
-  template:
-    metadata:
-      {{- with .Values.podAnnotations }}
-      annotations:
-        {{- toYaml . | nindent 8 }}
-      {{- end }}
-      labels:
-        {{- include "subproxy.selectorLabels" . | nindent 8 }}
-    spec:
-      {{- with .Values.imagePullSecrets }}
-      imagePullSecrets:
-        {{- toYaml . | nindent 8 }}
-      {{- end }}
-      serviceAccountName: {{ include "subproxy.serviceAccountName" . }}
-      securityContext:
-        {{- toYaml .Values.podSecurityContext | nindent 8 }}
-      containers:
-        - name: {{ .Chart.Name }}
-          securityContext:
-            {{- toYaml .Values.securityContext | nindent 12 }}
-          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
-          imagePullPolicy: {{ .Values.image.pullPolicy }}
-          env:
-          - name: WS_URL
-            value: {{ .Values.ws_url }}
-          args: [
-            "--substrate", "$(WS_URL)",
-          ]
-          ports:
-            - name: http
-              containerPort: 9950
-              protocol: TCP
-          resources:
-            {{- toYaml .Values.resources | nindent 12 }}
-      {{- with .Values.nodeSelector }}
-      nodeSelector:
-        {{- toYaml . | nindent 8 }}
-      {{- end }}
-      {{- with .Values.affinity }}
-      affinity:
-        {{- toYaml . | nindent 8 }}
-      {{- end }}
-      {{- with .Values.tolerations }}
-      tolerations:
-        {{- toYaml . | nindent 8 }}
-      {{- end }}
diff --git a/tools/subproxy/chart/templates/ingress.yaml b/tools/subproxy/chart/templates/ingress.yaml
deleted file mode 100644
index 4b7a22d5..00000000
--- a/tools/subproxy/chart/templates/ingress.yaml
+++ /dev/null
@@ -1,45 +0,0 @@
-{{- if .Values.ingress.enabled -}}
-{{- $fullName := include "subproxy.fullname" . -}}
-{{- $svcPort := .Values.service.port -}}
-{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
-apiVersion: networking.k8s.io/v1beta1
-{{- else -}}
-apiVersion: extensions/v1beta1
-{{- end }}
-kind: Ingress
-metadata:
-  name: {{ $fullName }}
-  labels:
-    {{- include "subproxy.labels" . | nindent 4 }}
-    backupType: {{ .Values.threefoldVdc.backup | quote }}
-  annotations:
-    {{- with .Values.ingress.annotations }}
-    {{- toYaml . | nindent 4 }}
-    {{- end }}
-    {{- if .Values.global.ingress.certresolver }}
-    traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.global.ingress.certresolver }}
-    {{- end }}
-spec:
-  {{- if .Values.ingress.tls }}
-  tls:
-    {{- range .Values.ingress.tls }}
-    - hosts:
-        {{- range .hosts }}
-        - {{ . | quote }}
-        {{- end }}
-      secretName: {{ .secretName }}
-    {{- end }}
-  {{- end }}
-  rules:
-    {{- range .Values.ingress.hosts }}
-    - host: {{ .host | quote }}
-      http:
-        paths:
-          {{- range .paths }}
-          - path: {{ . }}
-            backend:
-              serviceName: {{ $fullName }}
-              servicePort: {{ $svcPort }}
-          {{- end }}
-    {{- end }}
-  {{- end }}
diff --git a/tools/subproxy/chart/templates/service.yaml b/tools/subproxy/chart/templates/service.yaml
deleted file mode 100644
index 79d72f97..00000000
--- a/tools/subproxy/chart/templates/service.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: {{ include "subproxy.fullname" . }}
-  labels:
-    {{- include "subproxy.labels" . | nindent 4 }}
-spec:
-  type: {{ .Values.service.type }}
-  ports:
-    - port: {{ .Values.service.port }}
-      targetPort: 9950
-      protocol: TCP
-      name: http
-  selector:
-    {{- include "subproxy.selectorLabels" . | nindent 4 }}
diff --git a/tools/subproxy/chart/templates/serviceaccount.yaml b/tools/subproxy/chart/templates/serviceaccount.yaml
deleted file mode 100644
index 4fb5b403..00000000
--- a/tools/subproxy/chart/templates/serviceaccount.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-{{- if .Values.serviceAccount.create -}}
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: {{ include "subproxy.serviceAccountName" . }}
-  labels:
-    {{- include "subproxy.labels" . | nindent 4 }}
-  {{- with .Values.serviceAccount.annotations }}
-  annotations:
-    {{- toYaml . | nindent 4 }}
-  {{- end }}
-{{- end }}
diff --git a/tools/subproxy/chart/values.yaml b/tools/subproxy/chart/values.yaml
deleted file mode 100644
index 37bb9f16..00000000
--- a/tools/subproxy/chart/values.yaml
+++ /dev/null
@@ -1,77 +0,0 @@
-# Default values for substrate-node.
-# This is a YAML-formatted file.
-# Declare variables to be passed into your templates.
-
-image:
-  repository: subproxy
-  pullPolicy: IfNotPresent
-  tag: ""
-
-imagePullSecrets: []
-nameOverride: ""
-fullnameOverride: ""
-
-serviceAccount:
-  # Specifies whether a service account should be created
-  create: true
-  # Annotations to add to the service account
-  annotations: {}
-  # The name of the service account to use.
-  # If not set and create is true, a name is generated using the fullname template
-  name: ""
-
-podAnnotations: {}
-
-podSecurityContext:
-  {}
-  # fsGroup: 2000
-
-securityContext:
-  {}
-  # capabilities:
-  #   drop:
-  #   - ALL
-  # readOnlyRootFilesystem: true
-  # runAsNonRoot: true
-  # runAsUser: 1000
-
-service:
-  type: ClusterIP
-  port: 80
-
-ws_url: wss://tfchain.dev.threefold.io
-
-global:
-  ingress:
-    certresolver: le
-
-ingress:
-  enabled: false
-  annotations:
-  hosts:
-    - host: 
-      paths:
-        - /
-  tls: []
-
-resources:
-  {}
-  # We usually recommend not to specify default resources and to leave this as a conscious
-  # choice for the user. This also increases chances charts run on environments with little
-  # resources, such as Minikube. If you do want to specify resources, uncomment the following
-  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
-  # limits:
-  #   cpu: 100m
-  #   memory: 128Mi
-  # requests:
-  #   cpu: 100m
-  #   memory: 128Mi
-
-nodeSelector: {}
-
-tolerations: []
-
-affinity: {}
-
-threefoldVdc:
-  backup: ""
diff --git a/tools/subproxy/main.go b/tools/subproxy/main.go
deleted file mode 100644
index d2af19da..00000000
--- a/tools/subproxy/main.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"errors"
-	"flag"
-	"fmt"
-	"net/http"
-	"os"
-	"strconv"
-
-	"github.com/gorilla/handlers"
-	"github.com/gorilla/mux"
-	"github.com/rs/zerolog/log"
-	"github.com/threefoldtech/substrate-client"
-)
-
-func main() {
-	var (
-		url     string
-		address string
-	)
-
-	flag.StringVar(&url, "substrate", "", "websocket url to substrate")
-	flag.StringVar(&address, "address", ":9950", "listening address")
-
-	flag.Parse()
-
-	client, err := substrate.NewSubstrate(url)
-	if err != nil {
-		log.Error().Err(err).Msg("failed to connect to substrate")
-	}
-
-	router := mux.NewRouter()
-
-	router.Path("/twin/{id}").Methods(http.MethodGet).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		idStr := mux.Vars(r)["id"]
-		id, err := strconv.ParseUint(idStr, 10, 32)
-		if err != nil {
-			w.WriteHeader(http.StatusBadRequest)
-			fmt.Fprintf(w, "invalid twin id '%s': %s", idStr, err)
-			return
-		}
-		twin, err := client.GetTwin(uint32(id))
-		if errors.Is(err, substrate.ErrNotFound) {
-			w.WriteHeader(http.StatusNotFound)
-			return
-		} else if err != nil {
-			w.WriteHeader(http.StatusInternalServerError)
-			fmt.Fprintf(w, "failed to query twin: %s", err)
-			return
-		}
-
-		w.WriteHeader(http.StatusOK)
-		if err := json.NewEncoder(w).Encode(twin); err != nil {
-			log.Error().Err(err).Msg("failed to write response")
-		}
-	})
-
-	server := http.Server{
-		Addr:    address,
-		Handler: handlers.LoggingHandler(os.Stdout, router),
-	}
-
-	if err := server.ListenAndServe(); err != nil {
-		log.Error().Err(err).Msg("server exited")
-	}
-}


Test patch:
diff --git a/pkg/rrd/rrd_test.go b/pkg/rrd/rrd_test.go
index 8bd67e8f..31175629 100644
--- a/pkg/rrd/rrd_test.go
+++ b/pkg/rrd/rrd_test.go
@@ -218,3 +218,33 @@ func TestCountersLast(t *testing.T) {
 	require.EqualValues(120, v)
 
 }
+
+func TestCountersMultipleReports(t *testing.T) {
+	require := require.New(t)
+	path := filepath.Join(os.TempDir(), fmt.Sprint(rand.Int63()))
+	defer os.RemoveAll(path)
+
+	window := 5 * time.Minute
+	db, err := newRRDBolt(path, window, 24*time.Hour)
+	require.NoError(err)
+	now := time.Now()
+
+	lastReportTime := now.Unix()
+	total := 0.0
+	for i := 0; i <= 24; i++ {
+		slot, err := db.slotAt(uint64(now.Add(time.Duration(i) * 5 * time.Minute).Unix()))
+		require.NoError(err)
+		err = slot.Counter("test-0", float64(i))
+		require.NoError(err)
+		if i%6 == 0 && i != 0 {
+			counters, err := db.Counters(time.Unix(lastReportTime, 0))
+			require.NoError(err)
+			require.Len(counters, 1)
+			lastReportTime = now.Add(time.Duration(i) * 5 * time.Minute).Unix()
+			require.EqualValues(6, counters["test-0"])
+			total += counters["test-0"]
+		}
+	}
+
+	require.EqualValues(24, total)
+}