feat: curioweb: Sector info page (#11846)

* feat: curioweb: Sector info page

* Address review
This commit is contained in:
Łukasz Magiera 2024-04-11 18:30:53 +02:00 committed by GitHub
parent afa9032833
commit d7d849cf20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 553 additions and 284 deletions

View File

@ -4,10 +4,18 @@ import (
"context" "context"
"time" "time"
lru "github.com/hashicorp/golang-lru/v2"
blocks "github.com/ipfs/go-block-format"
"github.com/filecoin-project/lotus/api/client" "github.com/filecoin-project/lotus/api/client"
"github.com/filecoin-project/lotus/blockstore"
"github.com/filecoin-project/lotus/chain/store"
cliutil "github.com/filecoin-project/lotus/cli/util" cliutil "github.com/filecoin-project/lotus/cli/util"
"github.com/filecoin-project/lotus/lib/must"
) )
var ChainBlockCache = must.One(lru.New[blockstore.MhString, blocks.Block](4096))
func (a *app) watchRpc() { func (a *app) watchRpc() {
ticker := time.NewTicker(watchInterval) ticker := time.NewTicker(watchInterval)
for { for {
@ -84,6 +92,7 @@ func (a *app) updateRpc(ctx context.Context) error {
}() }()
a.workingApi = v1api a.workingApi = v1api
a.stor = store.ActorStore(ctx, blockstore.NewReadCachedBlockstore(blockstore.NewAPIBlockstore(a.workingApi), ChainBlockCache))
} }
} }

View File

@ -41,6 +41,9 @@ func Routes(r *mux.Router, deps *deps.Deps) error {
// node info page // node info page
r.HandleFunc("/node/{id}", a.nodeInfo) r.HandleFunc("/node/{id}", a.nodeInfo)
// sector info page
r.HandleFunc("/sector/{sp}/{id}", a.sectorInfo)
return nil return nil
} }

View File

@ -8,15 +8,23 @@ import (
"os" "os"
"sort" "sort"
"strconv" "strconv"
"strings"
"sync" "sync"
"text/template" "text/template"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/samber/lo"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/lotus/api/v1api" "github.com/filecoin-project/lotus/api/v1api"
"github.com/filecoin-project/lotus/chain/actors/adt"
"github.com/filecoin-project/lotus/lib/harmony/harmonydb" "github.com/filecoin-project/lotus/lib/harmony/harmonydb"
"github.com/filecoin-project/lotus/lib/must"
"github.com/filecoin-project/lotus/storage/paths"
"github.com/filecoin-project/lotus/storage/sealer/storiface"
) )
type app struct { type app struct {
@ -25,6 +33,7 @@ type app struct {
rpcInfoLk sync.Mutex rpcInfoLk sync.Mutex
workingApi v1api.FullNode workingApi v1api.FullNode
stor adt.Store
actorInfoLk sync.Mutex actorInfoLk sync.Mutex
actorInfos []actorInfo actorInfos []actorInfo
@ -128,6 +137,253 @@ func (a *app) nodeInfo(writer http.ResponseWriter, request *http.Request) {
a.executePageTemplate(writer, "node_info", "Node Info", mi) a.executePageTemplate(writer, "node_info", "Node Info", mi)
} }
func (a *app) sectorInfo(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id, ok := params["id"]
if !ok {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
intid, err := strconv.ParseInt(id, 10, 64)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
sp, ok := params["sp"]
if !ok {
http.Error(w, "missing sp", http.StatusBadRequest)
return
}
maddr, err := address.NewFromString(sp)
if err != nil {
http.Error(w, "invalid sp", http.StatusBadRequest)
return
}
spid, err := address.IDFromAddress(maddr)
if err != nil {
http.Error(w, "invalid sp", http.StatusBadRequest)
return
}
ctx := r.Context()
var tasks []PipelineTask
err = a.db.Select(ctx, &tasks, `SELECT
sp_id, sector_number,
create_time,
task_id_sdr, after_sdr,
task_id_tree_d, after_tree_d,
task_id_tree_c, after_tree_c,
task_id_tree_r, after_tree_r,
task_id_precommit_msg, after_precommit_msg,
after_precommit_msg_success, seed_epoch,
task_id_porep, porep_proof, after_porep,
task_id_finalize, after_finalize,
task_id_move_storage, after_move_storage,
task_id_commit_msg, after_commit_msg,
after_commit_msg_success,
failed, failed_reason
FROM sectors_sdr_pipeline WHERE sp_id = $1 AND sector_number = $2`, spid, intid)
if err != nil {
http.Error(w, xerrors.Errorf("failed to fetch pipeline task info: %w", err).Error(), http.StatusInternalServerError)
return
}
if len(tasks) == 0 {
http.Error(w, "sector not found", http.StatusInternalServerError)
return
}
head, err := a.workingApi.ChainHead(ctx)
if err != nil {
http.Error(w, xerrors.Errorf("failed to fetch chain head: %w", err).Error(), http.StatusInternalServerError)
return
}
epoch := head.Height()
mbf, err := a.getMinerBitfields(ctx, maddr, a.stor)
if err != nil {
http.Error(w, xerrors.Errorf("failed to load miner bitfields: %w", err).Error(), http.StatusInternalServerError)
return
}
task := tasks[0]
afterSeed := task.SeedEpoch != nil && *task.SeedEpoch <= int64(epoch)
var sectorLocations []struct {
CanSeal, CanStore bool
FileType storiface.SectorFileType `db:"sector_filetype"`
StorageID string `db:"storage_id"`
Urls string `db:"urls"`
}
err = a.db.Select(ctx, &sectorLocations, `SELECT p.can_seal, p.can_store, l.sector_filetype, l.storage_id, p.urls FROM sector_location l
JOIN storage_path p ON l.storage_id = p.storage_id
WHERE l.sector_num = $1 and l.miner_id = $2 ORDER BY p.can_seal, p.can_store, l.storage_id`, intid, spid)
if err != nil {
http.Error(w, xerrors.Errorf("failed to fetch sector locations: %w", err).Error(), http.StatusInternalServerError)
return
}
type fileLocations struct {
StorageID string
Urls []string
}
type locationTable struct {
PathType *string
PathTypeRowSpan int
FileType *string
FileTypeRowSpan int
Locations []fileLocations
}
locs := []locationTable{}
for i, loc := range sectorLocations {
loc := loc
urlList := strings.Split(loc.Urls, paths.URLSeparator)
fLoc := fileLocations{
StorageID: loc.StorageID,
Urls: urlList,
}
var pathTypeStr *string
var fileTypeStr *string
pathTypeRowSpan := 1
fileTypeRowSpan := 1
pathType := "None"
if loc.CanSeal && loc.CanStore {
pathType = "Seal/Store"
} else if loc.CanSeal {
pathType = "Seal"
} else if loc.CanStore {
pathType = "Store"
}
pathTypeStr = &pathType
fileType := loc.FileType.String()
fileTypeStr = &fileType
if i > 0 {
prevNonNilPathTypeLoc := i - 1
for prevNonNilPathTypeLoc > 0 && locs[prevNonNilPathTypeLoc].PathType == nil {
prevNonNilPathTypeLoc--
}
if *locs[prevNonNilPathTypeLoc].PathType == *pathTypeStr {
pathTypeRowSpan = 0
pathTypeStr = nil
locs[prevNonNilPathTypeLoc].PathTypeRowSpan++
// only if we have extended path type we may need to extend file type
prevNonNilFileTypeLoc := i - 1
for prevNonNilFileTypeLoc > 0 && locs[prevNonNilFileTypeLoc].FileType == nil {
prevNonNilFileTypeLoc--
}
if *locs[prevNonNilFileTypeLoc].FileType == *fileTypeStr {
fileTypeRowSpan = 0
fileTypeStr = nil
locs[prevNonNilFileTypeLoc].FileTypeRowSpan++
}
}
}
locTable := locationTable{
PathType: pathTypeStr,
PathTypeRowSpan: pathTypeRowSpan,
FileType: fileTypeStr,
FileTypeRowSpan: fileTypeRowSpan,
Locations: []fileLocations{fLoc},
}
locs = append(locs, locTable)
}
// TaskIDs
taskIDs := map[int64]struct{}{}
var htasks []taskSummary
{
// get non-nil task IDs
appendNonNil := func(id *int64) {
if id != nil {
taskIDs[*id] = struct{}{}
}
}
appendNonNil(task.TaskSDR)
appendNonNil(task.TaskTreeD)
appendNonNil(task.TaskTreeC)
appendNonNil(task.TaskTreeR)
appendNonNil(task.TaskPrecommitMsg)
appendNonNil(task.TaskPoRep)
appendNonNil(task.TaskFinalize)
appendNonNil(task.TaskMoveStorage)
appendNonNil(task.TaskCommitMsg)
if len(taskIDs) > 0 {
ids := lo.Keys(taskIDs)
var dbtasks []struct {
OwnerID *string `db:"owner_id"`
HostAndPort *string `db:"host_and_port"`
TaskID int64 `db:"id"`
Name string `db:"name"`
UpdateTime time.Time `db:"update_time"`
}
err = a.db.Select(ctx, &dbtasks, `SELECT t.owner_id, hm.host_and_port, t.id, t.name, t.update_time FROM harmony_task t LEFT JOIN curio.harmony_machines hm ON hm.id = t.owner_id WHERE t.id = ANY($1)`, ids)
if err != nil {
http.Error(w, fmt.Sprintf("failed to fetch task names: %v", err), http.StatusInternalServerError)
return
}
for _, tn := range dbtasks {
htasks = append(htasks, taskSummary{
Name: tn.Name,
SincePosted: time.Since(tn.UpdateTime.Local()).Round(time.Second).String(),
Owner: tn.HostAndPort,
OwnerID: tn.OwnerID,
ID: tn.TaskID,
})
}
}
}
mi := struct {
SectorNumber int64
PipelinePoRep sectorListEntry
Locations []locationTable
Tasks []taskSummary
}{
SectorNumber: intid,
PipelinePoRep: sectorListEntry{
PipelineTask: tasks[0],
AfterSeed: afterSeed,
ChainAlloc: must.One(mbf.alloc.IsSet(uint64(task.SectorNumber))),
ChainSector: must.One(mbf.sectorSet.IsSet(uint64(task.SectorNumber))),
ChainActive: must.One(mbf.active.IsSet(uint64(task.SectorNumber))),
ChainUnproven: must.One(mbf.unproven.IsSet(uint64(task.SectorNumber))),
ChainFaulty: must.One(mbf.faulty.IsSet(uint64(task.SectorNumber))),
},
Locations: locs,
Tasks: htasks,
}
a.executePageTemplate(w, "sector_info", "Sector Info", mi)
}
var templateDev = os.Getenv("LOTUS_WEB_DEV") == "1" var templateDev = os.Getenv("LOTUS_WEB_DEV") == "1"
func (a *app) executeTemplate(w http.ResponseWriter, name string, data interface{}) { func (a *app) executeTemplate(w http.ResponseWriter, name string, data interface{}) {

View File

@ -1,28 +1,21 @@
package hapi package hapi
import ( import (
"context"
"net/http" "net/http"
"time" "time"
lru "github.com/hashicorp/golang-lru/v2"
blocks "github.com/ipfs/go-block-format"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/filecoin-project/go-address" "github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-bitfield" "github.com/filecoin-project/go-bitfield"
"github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/chain/actors/adt"
"github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/actors/builtin/miner"
"github.com/filecoin-project/lotus/chain/store"
"github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/lib/must" "github.com/filecoin-project/lotus/lib/must"
) )
var ChainBlockCache = must.One(lru.New[blockstore.MhString, blocks.Block](4096))
func (a *app) pipelinePorepSectors(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
type PipelineTask struct { type PipelineTask struct {
SpID int64 `db:"sp_id"` SpID int64 `db:"sp_id"`
SectorNumber int64 `db:"sector_number"` SectorNumber int64 `db:"sector_number"`
@ -66,6 +59,23 @@ func (a *app) pipelinePorepSectors(w http.ResponseWriter, r *http.Request) {
FailedReason string `db:"failed_reason"` FailedReason string `db:"failed_reason"`
} }
type sectorListEntry struct {
PipelineTask
Address address.Address
CreateTime string
AfterSeed bool
ChainAlloc, ChainSector, ChainActive, ChainUnproven, ChainFaulty bool
}
type minerBitfields struct {
alloc, sectorSet, active, unproven, faulty bitfield.BitField
}
func (a *app) pipelinePorepSectors(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var tasks []PipelineTask var tasks []PipelineTask
err := a.db.Select(ctx, &tasks, `SELECT err := a.db.Select(ctx, &tasks, `SELECT
@ -89,16 +99,6 @@ func (a *app) pipelinePorepSectors(w http.ResponseWriter, r *http.Request) {
return return
} }
type sectorListEntry struct {
PipelineTask
Address address.Address
CreateTime string
AfterSeed bool
ChainAlloc, ChainSector, ChainActive, ChainUnproven, ChainFaulty bool
}
head, err := a.workingApi.ChainHead(ctx) head, err := a.workingApi.ChainHead(ctx)
if err != nil { if err != nil {
http.Error(w, xerrors.Errorf("failed to fetch chain head: %w", err).Error(), http.StatusInternalServerError) http.Error(w, xerrors.Errorf("failed to fetch chain head: %w", err).Error(), http.StatusInternalServerError)
@ -106,11 +106,6 @@ func (a *app) pipelinePorepSectors(w http.ResponseWriter, r *http.Request) {
} }
epoch := head.Height() epoch := head.Height()
stor := store.ActorStore(ctx, blockstore.NewReadCachedBlockstore(blockstore.NewAPIBlockstore(a.workingApi), ChainBlockCache))
type minerBitfields struct {
alloc, sectorSet, active, unproven, faulty bitfield.BitField
}
minerBitfieldCache := map[address.Address]minerBitfields{} minerBitfieldCache := map[address.Address]minerBitfields{}
sectorList := make([]sectorListEntry, 0, len(tasks)) sectorList := make([]sectorListEntry, 0, len(tasks))
@ -127,55 +122,11 @@ func (a *app) pipelinePorepSectors(w http.ResponseWriter, r *http.Request) {
mbf, ok := minerBitfieldCache[addr] mbf, ok := minerBitfieldCache[addr]
if !ok { if !ok {
act, err := a.workingApi.StateGetActor(ctx, addr, types.EmptyTSK) mbf, err := a.getMinerBitfields(ctx, addr, a.stor)
if err != nil { if err != nil {
http.Error(w, xerrors.Errorf("failed to load actor: %w", err).Error(), http.StatusInternalServerError) http.Error(w, xerrors.Errorf("failed to load miner bitfields: %w", err).Error(), http.StatusInternalServerError)
return return
} }
mas, err := miner.Load(stor, act)
if err != nil {
http.Error(w, xerrors.Errorf("failed to load miner actor: %w", err).Error(), http.StatusInternalServerError)
return
}
activeSectors, err := miner.AllPartSectors(mas, miner.Partition.ActiveSectors)
if err != nil {
http.Error(w, xerrors.Errorf("failed to load active sectors: %w", err).Error(), http.StatusInternalServerError)
return
}
allSectors, err := miner.AllPartSectors(mas, miner.Partition.AllSectors)
if err != nil {
http.Error(w, xerrors.Errorf("failed to load all sectors: %w", err).Error(), http.StatusInternalServerError)
return
}
unproved, err := miner.AllPartSectors(mas, miner.Partition.UnprovenSectors)
if err != nil {
http.Error(w, xerrors.Errorf("failed to load unproven sectors: %w", err).Error(), http.StatusInternalServerError)
return
}
faulty, err := miner.AllPartSectors(mas, miner.Partition.FaultySectors)
if err != nil {
http.Error(w, xerrors.Errorf("failed to load faulty sectors: %w", err).Error(), http.StatusInternalServerError)
return
}
alloc, err := mas.GetAllocatedSectors()
if err != nil {
http.Error(w, xerrors.Errorf("failed to load allocated sectors: %w", err).Error(), http.StatusInternalServerError)
return
}
mbf = minerBitfields{
alloc: *alloc,
sectorSet: allSectors,
active: activeSectors,
unproven: unproved,
faulty: faulty,
}
minerBitfieldCache[addr] = mbf minerBitfieldCache[addr] = mbf
} }
@ -197,3 +148,48 @@ func (a *app) pipelinePorepSectors(w http.ResponseWriter, r *http.Request) {
a.executeTemplate(w, "pipeline_porep_sectors", sectorList) a.executeTemplate(w, "pipeline_porep_sectors", sectorList)
} }
func (a *app) getMinerBitfields(ctx context.Context, addr address.Address, stor adt.Store) (minerBitfields, error) {
act, err := a.workingApi.StateGetActor(ctx, addr, types.EmptyTSK)
if err != nil {
return minerBitfields{}, xerrors.Errorf("failed to load actor: %w", err)
}
mas, err := miner.Load(stor, act)
if err != nil {
return minerBitfields{}, xerrors.Errorf("failed to load miner actor: %w", err)
}
activeSectors, err := miner.AllPartSectors(mas, miner.Partition.ActiveSectors)
if err != nil {
return minerBitfields{}, xerrors.Errorf("failed to load active sectors: %w", err)
}
allSectors, err := miner.AllPartSectors(mas, miner.Partition.AllSectors)
if err != nil {
return minerBitfields{}, xerrors.Errorf("failed to load all sectors: %w", err)
}
unproved, err := miner.AllPartSectors(mas, miner.Partition.UnprovenSectors)
if err != nil {
return minerBitfields{}, xerrors.Errorf("failed to load unproven sectors: %w", err)
}
faulty, err := miner.AllPartSectors(mas, miner.Partition.FaultySectors)
if err != nil {
return minerBitfields{}, xerrors.Errorf("failed to load faulty sectors: %w", err)
}
alloc, err := mas.GetAllocatedSectors()
if err != nil {
return minerBitfields{}, xerrors.Errorf("failed to load allocated sectors: %w", err)
}
return minerBitfields{
alloc: *alloc,
sectorSet: allSectors,
active: activeSectors,
unproven: unproved,
faulty: faulty,
}, nil
}

View File

@ -1,9 +1,4 @@
{{define "pipeline_porep_sectors"}} {{define "sector_porep_state"}}
{{range .}}
<tr>
<td>{{.Address}}</td>
<td rowspan="2">{{.CreateTime}}</td>
<td rowspan="2">
<table class="porep-state"> <table class="porep-state">
<tbody> <tbody>
<tr> <tr>
@ -124,9 +119,18 @@
</td> </td>
</tbody> </tbody>
</table> </table>
{{end}}
{{define "pipeline_porep_sectors"}}
{{range .}}
<tr>
<td>{{.Address}}</td>
<td rowspan="2">{{.CreateTime}}</td>
<td rowspan="2">
{{template "sector_porep_state" .}}
</td> </td>
<td rowspan="2"> <td rowspan="2">
<a href="#">DETAILS</a> <a href="/hapi/sector/{{.Address}}/{{.SectorNumber}}">DETAILS</a>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -68,3 +68,53 @@ a:hover {
color: deeppink; color: deeppink;
} }
} }
/* Path: curiosrc/web/hapi/web/pipeline_porep_sectors.gohtml */
.porep-pipeline-table, .porep-state {
color: #d0d0d0;
}
.porep-pipeline-table td, .porep-pipeline-table th {
border-left: none;
border-collapse: collapse;
}
.porep-pipeline-table tr:nth-child(odd) {
border-top: 6px solid #999999;
}
.porep-pipeline-table tr:first-child, .porep-pipeline-table tr:first-child {
border-top: none;
}
.porep-state {
border-collapse: collapse;
}
.porep-state td, .porep-state th {
border-left: 1px solid #f0f0f0;
border-right: 1px solid #f0f0f0;
padding: 1px 5px;
text-align: center;
font-size: 0.7em;
}
.porep-state tr {
border-top: 1px solid #f0f0f0;
}
.porep-state tr:first-child {
border-top: none;
}
.pipeline-active {
background-color: #303060;
}
.pipeline-success {
background-color: #306030;
}
.pipeline-failed {
background-color: #603030;
}

View File

@ -5,57 +5,6 @@
<script type="module" src="chain-connectivity.js"></script> <script type="module" src="chain-connectivity.js"></script>
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/hack-font@3.3.0/build/web/hack-subset.css'> <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/hack-font@3.3.0/build/web/hack-subset.css'>
<style>
.porep-pipeline-table, .porep-state {
color: #d0d0d0;
}
.porep-pipeline-table td, .porep-pipeline-table th {
border-left: none;
border-collapse: collapse;
}
.porep-pipeline-table tr:nth-child(odd) {
border-top: 6px solid #999999;
}
.porep-pipeline-table tr:first-child, .porep-pipeline-table tr:first-child {
border-top: none;
}
.porep-state {
border-collapse: collapse;
}
.porep-state td, .porep-state th {
border-left: 1px solid #f0f0f0;
border-right: 1px solid #f0f0f0;
padding: 1px 5px;
text-align: center;
font-size: 0.7em;
}
.porep-state tr {
border-top: 1px solid #f0f0f0;
}
.porep-state tr:first-child {
border-top: none;
}
.pipeline-active {
background-color: #303060;
}
.pipeline-success {
background-color: #306030;
}
.pipeline-failed {
background-color: #603030;
}
</style>
</head> </head>
<body> <body>
<div class="app-head"> <div class="app-head">

View File

@ -26,6 +26,8 @@ import (
const NoMinerFilter = abi.ActorID(0) const NoMinerFilter = abi.ActorID(0)
const URLSeparator = ","
var errAlreadyLocked = errors.New("already locked") var errAlreadyLocked = errors.New("already locked")
type DBIndex struct { type DBIndex struct {
@ -197,13 +199,13 @@ func (dbi *DBIndex) StorageAttach(ctx context.Context, si storiface.StorageInfo,
if storageId.Valid { if storageId.Valid {
var currUrls []string var currUrls []string
if urls.Valid { if urls.Valid {
currUrls = strings.Split(urls.String, ",") currUrls = strings.Split(urls.String, URLSeparator)
} }
currUrls = union(currUrls, si.URLs) currUrls = union(currUrls, si.URLs)
_, err = tx.Exec( _, err = tx.Exec(
"UPDATE storage_path set urls=$1, weight=$2, max_storage=$3, can_seal=$4, can_store=$5, groups=$6, allow_to=$7, allow_types=$8, deny_types=$9, allow_miners=$10, deny_miners=$11, last_heartbeat=NOW() WHERE storage_id=$12", "UPDATE storage_path set urls=$1, weight=$2, max_storage=$3, can_seal=$4, can_store=$5, groups=$6, allow_to=$7, allow_types=$8, deny_types=$9, allow_miners=$10, deny_miners=$11, last_heartbeat=NOW() WHERE storage_id=$12",
strings.Join(currUrls, ","), strings.Join(currUrls, URLSeparator),
si.Weight, si.Weight,
si.MaxStorage, si.MaxStorage,
si.CanSeal, si.CanSeal,
@ -277,7 +279,7 @@ func (dbi *DBIndex) StorageDetach(ctx context.Context, id storiface.ID, url stri
} }
if len(modUrls) > 0 { if len(modUrls) > 0 {
newUrls := strings.Join(modUrls, ",") newUrls := strings.Join(modUrls, URLSeparator)
_, err := dbi.harmonyDB.Exec(ctx, "UPDATE storage_path set urls=$1 WHERE storage_id=$2", newUrls, id) _, err := dbi.harmonyDB.Exec(ctx, "UPDATE storage_path set urls=$1 WHERE storage_id=$2", newUrls, id)
if err != nil { if err != nil {
return err return err