ipfs-cluster/api/rest/restapi.go
Hector Sanjuan 9b9d76f92d Pinset streaming and method type revamp
This commit introduces the new go-libp2p-gorpc streaming capabilities for
Cluster. The main aim is to work towards heavily reducing memory usage when
working with very large pinsets.

As a side-effect, it takes the chance to revampt all types for all public
methods so that pointers to static what should be static objects are not used
anymore. This should heavily reduce heap allocations and GC activity.

The main change is that state.List now returns a channel from which to read
the pins, rather than pins being all loaded into a huge slice.

Things reading pins have been all updated to iterate on the channel rather
than on the slice. The full pinset is no longer fully loaded onto memory for
things that run regularly like StateSync().

Additionally, the /allocations endpoint of the rest API no longer returns an
array of pins, but rather streams json-encoded pin objects directly. This
change has extended to the restapi client (which puts pins into a channel as
they arrive) and to ipfs-cluster-ctl.

There are still pending improvements like StatusAll() calls which should also
stream responses, and specially BlockPut calls which should stream blocks
directly into IPFS on a single call.

These are coming up in future commits.
2022-03-19 03:02:55 +01:00

784 lines
17 KiB
Go

// Package rest implements an IPFS Cluster API component. It provides
// a REST-ish API to interact with Cluster.
//
// The implented API is based on the common.API component (refer to module
// description there). The only thing this module does is to provide route
// handling for the otherwise common API component.
package rest
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"github.com/ipfs/go-cid"
"github.com/ipfs/ipfs-cluster/adder/adderutils"
types "github.com/ipfs/ipfs-cluster/api"
"github.com/ipfs/ipfs-cluster/api/common"
"go.uber.org/multierr"
logging "github.com/ipfs/go-log/v2"
"github.com/libp2p/go-libp2p-core/host"
peer "github.com/libp2p/go-libp2p-core/peer"
rpc "github.com/libp2p/go-libp2p-gorpc"
mux "github.com/gorilla/mux"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
var (
logger = logging.Logger("restapi")
apiLogger = logging.Logger("restapilog")
)
type peerAddBody struct {
PeerID string `json:"peer_id"`
}
// API implements the REST API Component.
// It embeds a common.API.
type API struct {
*common.API
rpcClient *rpc.Client
config *Config
}
// NewAPI creates a new REST API component.
func NewAPI(ctx context.Context, cfg *Config) (*API, error) {
return NewAPIWithHost(ctx, cfg, nil)
}
// NewAPI creates a new REST API component using the given libp2p Host.
func NewAPIWithHost(ctx context.Context, cfg *Config, h host.Host) (*API, error) {
api := API{
config: cfg,
}
capi, err := common.NewAPIWithHost(ctx, &cfg.Config, h, api.routes)
api.API = capi
return &api, err
}
// Routes returns endpoints supported by this API.
func (api *API) routes(c *rpc.Client) []common.Route {
api.rpcClient = c
return []common.Route{
{
Name: "ID",
Method: "GET",
Pattern: "/id",
HandlerFunc: api.idHandler,
},
{
Name: "Version",
Method: "GET",
Pattern: "/version",
HandlerFunc: api.versionHandler,
},
{
Name: "Peers",
Method: "GET",
Pattern: "/peers",
HandlerFunc: api.peerListHandler,
},
{
Name: "PeerAdd",
Method: "POST",
Pattern: "/peers",
HandlerFunc: api.peerAddHandler,
},
{
Name: "PeerRemove",
Method: "DELETE",
Pattern: "/peers/{peer}",
HandlerFunc: api.peerRemoveHandler,
},
{
Name: "Add",
Method: "POST",
Pattern: "/add",
HandlerFunc: api.addHandler,
},
{
Name: "Allocations",
Method: "GET",
Pattern: "/allocations",
HandlerFunc: api.allocationsHandler,
},
{
Name: "Allocation",
Method: "GET",
Pattern: "/allocations/{hash}",
HandlerFunc: api.allocationHandler,
},
{
Name: "StatusAll",
Method: "GET",
Pattern: "/pins",
HandlerFunc: api.statusAllHandler,
},
{
Name: "Recover",
Method: "POST",
Pattern: "/pins/{hash}/recover",
HandlerFunc: api.recoverHandler,
},
{
Name: "RecoverAll",
Method: "POST",
Pattern: "/pins/recover",
HandlerFunc: api.recoverAllHandler,
},
{
Name: "Status",
Method: "GET",
Pattern: "/pins/{hash}",
HandlerFunc: api.statusHandler,
},
{
Name: "Pin",
Method: "POST",
Pattern: "/pins/{hash}",
HandlerFunc: api.pinHandler,
},
{
Name: "PinPath",
Method: "POST",
Pattern: "/pins/{keyType:ipfs|ipns|ipld}/{path:.*}",
HandlerFunc: api.pinPathHandler,
},
{
Name: "Unpin",
Method: "DELETE",
Pattern: "/pins/{hash}",
HandlerFunc: api.unpinHandler,
},
{
Name: "UnpinPath",
Method: "DELETE",
Pattern: "/pins/{keyType:ipfs|ipns|ipld}/{path:.*}",
HandlerFunc: api.unpinPathHandler,
},
{
Name: "RepoGC",
Method: "POST",
Pattern: "/ipfs/gc",
HandlerFunc: api.repoGCHandler,
},
{
Name: "ConnectionGraph",
Method: "GET",
Pattern: "/health/graph",
HandlerFunc: api.graphHandler,
},
{
Name: "Alerts",
Method: "GET",
Pattern: "/health/alerts",
HandlerFunc: api.alertsHandler,
},
{
Name: "Metrics",
Method: "GET",
Pattern: "/monitor/metrics/{name}",
HandlerFunc: api.metricsHandler,
},
{
Name: "MetricNames",
Method: "GET",
Pattern: "/monitor/metrics",
HandlerFunc: api.metricNamesHandler,
},
}
}
func (api *API) idHandler(w http.ResponseWriter, r *http.Request) {
var id types.ID
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"ID",
struct{}{},
&id,
)
api.SendResponse(w, common.SetStatusAutomatically, err, &id)
}
func (api *API) versionHandler(w http.ResponseWriter, r *http.Request) {
var v types.Version
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Version",
struct{}{},
&v,
)
api.SendResponse(w, common.SetStatusAutomatically, err, v)
}
func (api *API) graphHandler(w http.ResponseWriter, r *http.Request) {
var graph types.ConnectGraph
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"ConnectGraph",
struct{}{},
&graph,
)
api.SendResponse(w, common.SetStatusAutomatically, err, graph)
}
func (api *API) metricsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
var metrics []types.Metric
err := api.rpcClient.CallContext(
r.Context(),
"",
"PeerMonitor",
"LatestMetrics",
name,
&metrics,
)
api.SendResponse(w, common.SetStatusAutomatically, err, metrics)
}
func (api *API) metricNamesHandler(w http.ResponseWriter, r *http.Request) {
var metricNames []string
err := api.rpcClient.CallContext(
r.Context(),
"",
"PeerMonitor",
"MetricNames",
struct{}{},
&metricNames,
)
api.SendResponse(w, common.SetStatusAutomatically, err, metricNames)
}
func (api *API) alertsHandler(w http.ResponseWriter, r *http.Request) {
var alerts []types.Alert
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Alerts",
struct{}{},
&alerts,
)
api.SendResponse(w, common.SetStatusAutomatically, err, alerts)
}
func (api *API) addHandler(w http.ResponseWriter, r *http.Request) {
reader, err := r.MultipartReader()
if err != nil {
api.SendResponse(w, http.StatusBadRequest, err, nil)
return
}
params, err := types.AddParamsFromQuery(r.URL.Query())
if err != nil {
api.SendResponse(w, http.StatusBadRequest, err, nil)
return
}
api.SetHeaders(w)
// any errors sent as trailer
adderutils.AddMultipartHTTPHandler(
r.Context(),
api.rpcClient,
params,
reader,
w,
nil,
)
}
func (api *API) peerListHandler(w http.ResponseWriter, r *http.Request) {
var peers []types.ID
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Peers",
struct{}{},
&peers,
)
api.SendResponse(w, common.SetStatusAutomatically, err, peers)
}
func (api *API) peerAddHandler(w http.ResponseWriter, r *http.Request) {
dec := json.NewDecoder(r.Body)
defer r.Body.Close()
var addInfo peerAddBody
err := dec.Decode(&addInfo)
if err != nil {
api.SendResponse(w, http.StatusBadRequest, errors.New("error decoding request body"), nil)
return
}
pid, err := peer.Decode(addInfo.PeerID)
if err != nil {
api.SendResponse(w, http.StatusBadRequest, errors.New("error decoding peer_id"), nil)
return
}
var id types.ID
err = api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PeerAdd",
pid,
&id,
)
api.SendResponse(w, common.SetStatusAutomatically, err, &id)
}
func (api *API) peerRemoveHandler(w http.ResponseWriter, r *http.Request) {
if p := api.ParsePidOrFail(w, r); p != "" {
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PeerRemove",
p,
&struct{}{},
)
api.SendResponse(w, common.SetStatusAutomatically, err, nil)
}
}
func (api *API) pinHandler(w http.ResponseWriter, r *http.Request) {
if pin := api.ParseCidOrFail(w, r); pin.Defined() {
api.config.Logger.Debugf("rest api pinHandler: %s", pin.Cid)
// span.AddAttributes(trace.StringAttribute("cid", pin.Cid))
var pinObj types.Pin
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Pin",
pin,
&pinObj,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinObj)
api.config.Logger.Debug("rest api pinHandler done")
}
}
func (api *API) unpinHandler(w http.ResponseWriter, r *http.Request) {
if pin := api.ParseCidOrFail(w, r); pin.Defined() {
api.config.Logger.Debugf("rest api unpinHandler: %s", pin.Cid)
// span.AddAttributes(trace.StringAttribute("cid", pin.Cid))
var pinObj types.Pin
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Unpin",
pin,
&pinObj,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinObj)
api.config.Logger.Debug("rest api unpinHandler done")
}
}
func (api *API) pinPathHandler(w http.ResponseWriter, r *http.Request) {
var pin types.Pin
if pinpath := api.ParsePinPathOrFail(w, r); pinpath.Defined() {
api.config.Logger.Debugf("rest api pinPathHandler: %s", pinpath.Path)
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PinPath",
pinpath,
&pin,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pin)
api.config.Logger.Debug("rest api pinPathHandler done")
}
}
func (api *API) unpinPathHandler(w http.ResponseWriter, r *http.Request) {
var pin types.Pin
if pinpath := api.ParsePinPathOrFail(w, r); pinpath.Defined() {
api.config.Logger.Debugf("rest api unpinPathHandler: %s", pinpath.Path)
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"UnpinPath",
pinpath,
&pin,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pin)
api.config.Logger.Debug("rest api unpinPathHandler done")
}
}
func (api *API) allocationsHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
filterStr := queryValues.Get("filter")
var filter types.PinType
for _, f := range strings.Split(filterStr, ",") {
filter |= types.PinTypeFromString(f)
}
if filter == types.BadType {
api.SendResponse(w, http.StatusBadRequest, errors.New("invalid filter value"), nil)
return
}
in := make(chan struct{})
close(in)
pins := make(chan types.Pin)
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
go func() {
err := api.rpcClient.Stream(
r.Context(),
"",
"Cluster",
"Pins",
in,
pins,
)
if err != nil {
logger.Error(err)
cancel()
}
}()
iter := func() (interface{}, bool, error) {
var p types.Pin
var ok bool
iterloop:
for {
select {
case <-ctx.Done():
break iterloop
case p, ok = <-pins:
if !ok {
break iterloop
}
// this means we keep iterating if no filter
// matched
if filter == types.AllType || filter&p.Type > 0 {
break iterloop
}
}
}
return p, ok, ctx.Err()
}
api.StreamResponse(w, iter)
}
func (api *API) allocationHandler(w http.ResponseWriter, r *http.Request) {
if pin := api.ParseCidOrFail(w, r); pin.Defined() {
var pinResp types.Pin
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PinGet",
pin.Cid,
&pinResp,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinResp)
}
}
func (api *API) statusAllHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
if queryValues.Get("cids") != "" {
api.statusCidsHandler(w, r)
return
}
local := queryValues.Get("local")
var globalPinInfos []types.GlobalPinInfo
filterStr := queryValues.Get("filter")
filter := types.TrackerStatusFromString(filterStr)
// FIXME: This is a bit lazy, as "invalidxx,pinned" would result in a
// valid "pinned" filter.
if filter == types.TrackerStatusUndefined && filterStr != "" {
api.SendResponse(w, http.StatusBadRequest, errors.New("invalid filter value"), nil)
return
}
if local == "true" {
var pinInfos []types.PinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"StatusAllLocal",
filter,
&pinInfos,
)
if err != nil {
api.SendResponse(w, common.SetStatusAutomatically, err, nil)
return
}
globalPinInfos = pinInfosToGlobal(pinInfos)
} else {
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"StatusAll",
filter,
&globalPinInfos,
)
if err != nil {
api.SendResponse(w, common.SetStatusAutomatically, err, nil)
return
}
}
api.SendResponse(w, common.SetStatusAutomatically, nil, globalPinInfos)
}
// request statuses for multiple CIDs in parallel.
func (api *API) statusCidsHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
filterCidsStr := strings.Split(queryValues.Get("cids"), ",")
var cids []cid.Cid
for _, cidStr := range filterCidsStr {
c, err := cid.Decode(cidStr)
if err != nil {
api.SendResponse(w, http.StatusBadRequest, fmt.Errorf("error decoding Cid: %w", err), nil)
return
}
cids = append(cids, c)
}
local := queryValues.Get("local")
type gpiResult struct {
gpi types.GlobalPinInfo
err error
}
gpiCh := make(chan gpiResult, len(cids))
var wg sync.WaitGroup
wg.Add(len(cids))
// Close channel when done
go func() {
wg.Wait()
close(gpiCh)
}()
if local == "true" {
for _, ci := range cids {
go func(c cid.Cid) {
defer wg.Done()
var pinInfo types.PinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"StatusLocal",
c,
&pinInfo,
)
gpiCh <- gpiResult{gpi: pinInfo.ToGlobal(), err: err}
}(ci)
}
} else {
for _, ci := range cids {
go func(c cid.Cid) {
defer wg.Done()
var pinInfo types.GlobalPinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Status",
c,
&pinInfo,
)
gpiCh <- gpiResult{gpi: pinInfo, err: err}
}(ci)
}
}
var gpis []types.GlobalPinInfo
var err error
for gpiResult := range gpiCh {
gpis = append(gpis, gpiResult.gpi)
err = multierr.Append(err, gpiResult.err)
}
api.SendResponse(w, common.SetStatusAutomatically, err, gpis)
}
func (api *API) statusHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if pin := api.ParseCidOrFail(w, r); pin.Defined() {
if local == "true" {
var pinInfo types.PinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"StatusLocal",
pin.Cid,
&pinInfo,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinInfo.ToGlobal())
} else {
var pinInfo types.GlobalPinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Status",
pin.Cid,
&pinInfo,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinInfo)
}
}
}
func (api *API) recoverAllHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if local == "true" {
var pinInfos []types.PinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"RecoverAllLocal",
struct{}{},
&pinInfos,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinInfosToGlobal(pinInfos))
} else {
var globalPinInfos []types.GlobalPinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"RecoverAll",
struct{}{},
&globalPinInfos,
)
api.SendResponse(w, common.SetStatusAutomatically, err, globalPinInfos)
}
}
func (api *API) recoverHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if pin := api.ParseCidOrFail(w, r); pin.Defined() {
if local == "true" {
var pinInfo types.PinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"RecoverLocal",
pin.Cid,
&pinInfo,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinInfo.ToGlobal())
} else {
var pinInfo types.GlobalPinInfo
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Recover",
pin.Cid,
&pinInfo,
)
api.SendResponse(w, common.SetStatusAutomatically, err, pinInfo)
}
}
}
func (api *API) repoGCHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if local == "true" {
var localRepoGC types.RepoGC
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"RepoGCLocal",
struct{}{},
&localRepoGC,
)
api.SendResponse(w, common.SetStatusAutomatically, err, repoGCToGlobal(localRepoGC))
return
}
var repoGC types.GlobalRepoGC
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"RepoGC",
struct{}{},
&repoGC,
)
api.SendResponse(w, common.SetStatusAutomatically, err, repoGC)
}
func repoGCToGlobal(r types.RepoGC) types.GlobalRepoGC {
return types.GlobalRepoGC{
PeerMap: map[string]types.RepoGC{
peer.Encode(r.Peer): r,
},
}
}
func pinInfosToGlobal(pInfos []types.PinInfo) []types.GlobalPinInfo {
gPInfos := make([]types.GlobalPinInfo, len(pInfos))
for i, p := range pInfos {
gpi := p.ToGlobal()
gPInfos[i] = gpi
}
return gPInfos
}