Compare commits

..

No commits in common. "23d02b1c9986801b0ef951a1c79fff2ef8ce832c" and "e6647e47f74ae41b36550fc13d0b0b511ff2c382" have entirely different histories.

11 changed files with 76 additions and 197 deletions

1
.gitignore vendored
View File

@ -3,4 +3,3 @@ output/
feta.sqlite feta.sqlite
.lintsetup .lintsetup
out out
debug.log

View File

@ -26,7 +26,7 @@ endif
default: build default: build
debug: build debug: build
GOTRACEBACK=all FETA_DEBUG=1 ./$(FN) 2>&1 | tee -a debug.log GOTRACEBACK=all FETA_DEBUG=1 ./$(FN)
run: build run: build
./$(FN) ./$(FN)

View File

@ -2,8 +2,9 @@ package database
import ( import (
"git.eeqj.de/sneak/feta/instance" "git.eeqj.de/sneak/feta/instance"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
_ "github.com/jinzhu/gorm/dialects/sqlite"
) )
func (m *Manager) SaveInstance(i *instance.Instance) error { func (m *Manager) SaveInstance(i *instance.Instance) error {
@ -19,7 +20,6 @@ func (m *Manager) SaveInstance(i *instance.Instance) error {
UUID: i.UUID, UUID: i.UUID,
Disabled: i.Disabled, Disabled: i.Disabled,
ErrorCount: i.ErrorCount, ErrorCount: i.ErrorCount,
ConsecutiveErrorCount: i.ConsecutiveErrorCount,
FSMState: i.Status(), FSMState: i.Status(),
Fetching: i.Fetching, Fetching: i.Fetching,
HighestID: i.HighestID, HighestID: i.HighestID,
@ -46,7 +46,6 @@ func (m *Manager) SaveInstance(i *instance.Instance) error {
m.db.Where("UUID = ?", i.UUID).First(&ei) m.db.Where("UUID = ?", i.UUID).First(&ei)
ei.Disabled = i.Disabled ei.Disabled = i.Disabled
ei.ErrorCount = i.ErrorCount ei.ErrorCount = i.ErrorCount
ei.ConsecutiveErrorCount = i.ConsecutiveErrorCount
ei.FSMState = i.Status() ei.FSMState = i.Status()
ei.Fetching = i.Fetching ei.Fetching = i.Fetching
ei.HighestID = i.HighestID ei.HighestID = i.HighestID
@ -75,7 +74,6 @@ func (m *Manager) ListInstances() ([]*instance.Instance, error) {
x.UUID = i.UUID x.UUID = i.UUID
x.Disabled = i.Disabled x.Disabled = i.Disabled
x.ErrorCount = i.ErrorCount x.ErrorCount = i.ErrorCount
x.ConsecutiveErrorCount = i.ConsecutiveErrorCount
x.InitialFSMState = i.FSMState x.InitialFSMState = i.FSMState
x.Fetching = i.Fetching x.Fetching = i.Fetching
x.HighestID = i.HighestID x.HighestID = i.HighestID

View File

@ -27,7 +27,6 @@ type StoredToot struct {
type APInstance struct { type APInstance struct {
gorm.Model gorm.Model
UUID uuid.UUID `gorm:"type:uuid;primary_key;"` UUID uuid.UUID `gorm:"type:uuid;primary_key;"`
ConsecutiveErrorCount uint
ErrorCount uint ErrorCount uint
SuccessCount uint SuccessCount uint
HighestID uint HighestID uint

View File

@ -14,16 +14,6 @@ func (m *Manager) TootCountForHostname(hostname string) (uint, error) {
} }
} }
func (m *Manager) TotalTootCount() (uint, error) {
var c uint
e := m.db.Model(&StoredToot{}).Count(&c)
if e.Error != nil {
return 0, e.Error
} else {
return c, nil
}
}
func (m *Manager) GetAPInstanceFromUUID(uuid *uuid.UUID) (*APInstance, error) { func (m *Manager) GetAPInstanceFromUUID(uuid *uuid.UUID) (*APInstance, error) {
var i APInstance var i APInstance
e := m.db.Model(&APInstance{}).Where("uuid = ?", uuid).First(&i) e := m.db.Model(&APInstance{}).Where("uuid = ?", uuid).First(&i)

View File

@ -17,19 +17,18 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
//import "github.com/gin-gonic/gin"
const nodeInfoSchemaVersionTwoName = "http://nodeinfo.diaspora.software/ns/schema/2.0" const nodeInfoSchemaVersionTwoName = "http://nodeinfo.diaspora.software/ns/schema/2.0"
const instanceNodeinfoTimeout = time.Second * 60 * 2 // 2m const instanceNodeinfoTimeout = time.Second * 50
const instanceHTTPTimeout = time.Second * 60 * 2 // 2m const instanceHTTPTimeout = time.Second * 120
const instanceSpiderInterval = time.Second * 60 * 2 // 2m const instanceSpiderInterval = time.Second * 120
const instanceErrorInterval = time.Second * 60 * 60 // 1h const instanceErrorInterval = time.Second * 60 * 30
const instancePersistentErrorInterval = time.Second * 86400 // 1d
const zeroInterval = time.Second * 0 // 0s
// Instance stores all the information we know about an instance // Instance stores all the information we know about an instance
type Instance struct { type Instance struct {
Disabled bool Disabled bool
ErrorCount uint ErrorCount uint
ConsecutiveErrorCount uint
FSM *fsm.FSM FSM *fsm.FSM
Fetching bool Fetching bool
HighestID uint HighestID uint
@ -62,10 +61,6 @@ func New(options ...func(i *Instance)) *Instance {
opt(i) opt(i)
} }
if i.InitialFSMState == "FETCHING" {
i.InitialFSMState = "READY_FOR_TOOTFETCH"
}
i.FSM = fsm.NewFSM( i.FSM = fsm.NewFSM(
i.InitialFSMState, i.InitialFSMState,
fsm.Events{ fsm.Events{
@ -79,13 +74,11 @@ func New(options ...func(i *Instance)) *Instance {
{Name: "EARLY_FETCH_ERROR", Src: []string{"FETCHING_NODEINFO_URL", "PRE_NODEINFO_FETCH", "FETCHING_NODEINFO"}, Dst: "EARLY_ERROR"}, {Name: "EARLY_FETCH_ERROR", Src: []string{"FETCHING_NODEINFO_URL", "PRE_NODEINFO_FETCH", "FETCHING_NODEINFO"}, Dst: "EARLY_ERROR"},
{Name: "TOOT_FETCH_ERROR", Src: []string{"FETCHING"}, Dst: "TOOT_FETCH_ERROR"}, {Name: "TOOT_FETCH_ERROR", Src: []string{"FETCHING"}, Dst: "TOOT_FETCH_ERROR"},
{Name: "TOOTS_FETCHED", Src: []string{"FETCHING"}, Dst: "READY_FOR_TOOTFETCH"}, {Name: "TOOTS_FETCHED", Src: []string{"FETCHING"}, Dst: "READY_FOR_TOOTFETCH"},
{Name: "DISABLEMENT", Src: []string{"WEIRD_NODE", "EARLY_ERROR", "TOOT_FETCH_ERROR"}, Dst: "DISABLED"},
}, },
fsm.Callbacks{ fsm.Callbacks{
"enter_state": func(e *fsm.Event) { i.fsmEnterState(e) }, "enter_state": func(e *fsm.Event) { i.fsmEnterState(e) },
}, },
) )
return i return i
} }
@ -128,36 +121,10 @@ func (i *Instance) Unlock() {
i.structLock.Unlock() i.structLock.Unlock()
} }
func (i *Instance) bumpFetchError() { func (i *Instance) bumpFetch() {
i.Lock() i.Lock()
probablyDead := i.ConsecutiveErrorCount > 3 defer i.Unlock()
shouldDisable := i.ConsecutiveErrorCount > 6 i.NextFetch = time.Now().Add(120 * time.Second)
i.Unlock()
if shouldDisable {
// auf wiedersehen, felicia
i.Lock()
i.Disabled = true
i.Unlock()
i.Event("DISABLEMENT")
return
}
if probablyDead {
// if three consecutive fetch errors happen, only try once per day:
i.setNextFetchAfter(instancePersistentErrorInterval)
} else {
// otherwise give them 1h
i.setNextFetchAfter(instanceErrorInterval)
}
}
func (i *Instance) bumpFetchSuccess() {
i.setNextFetchAfter(instanceSpiderInterval)
}
func (i *Instance) scheduleFetchImmediate() {
i.setNextFetchAfter(zeroInterval)
} }
func (i *Instance) setNextFetchAfter(d time.Duration) { func (i *Instance) setNextFetchAfter(d time.Duration) {
@ -172,7 +139,8 @@ func (i *Instance) Fetch() {
i.fetchingLock.Lock() i.fetchingLock.Lock()
defer i.fetchingLock.Unlock() defer i.fetchingLock.Unlock()
i.bumpFetchError() i.setNextFetchAfter(instanceErrorInterval)
err := i.DetectNodeTypeIfNecessary() err := i.DetectNodeTypeIfNecessary()
if err != nil { if err != nil {
log.Debug(). log.Debug().
@ -181,7 +149,8 @@ func (i *Instance) Fetch() {
Msg("unable to fetch instance metadata") Msg("unable to fetch instance metadata")
return return
} }
i.scheduleFetchImmediate()
i.setNextFetchAfter(instanceSpiderInterval)
log.Info(). log.Info().
Str("hostname", i.Hostname). Str("hostname", i.Hostname).
Msg("instance now ready for fetch") Msg("instance now ready for fetch")
@ -238,14 +207,12 @@ func (i *Instance) registerError() {
i.Lock() i.Lock()
defer i.Unlock() defer i.Unlock()
i.ErrorCount++ i.ErrorCount++
i.ConsecutiveErrorCount++
} }
func (i *Instance) registerSuccess() { func (i *Instance) registerSuccess() {
i.Lock() i.Lock()
defer i.Unlock() defer i.Unlock()
i.SuccessCount++ i.SuccessCount++
i.ConsecutiveErrorCount = 0
} }
// Up returns true if the success count is >0 // Up returns true if the success count is >0
@ -438,12 +405,9 @@ func (i *Instance) fetchRecentToots() error {
// it turns out pleroma supports the mastodon api so we'll just use that // it turns out pleroma supports the mastodon api so we'll just use that
// for everything for now // for everything for now
// FIXME would be nice to support non-https
url := fmt.Sprintf("https://%s/api/v1/timelines/public?limit=40&local=true", url := fmt.Sprintf("https://%s/api/v1/timelines/public?limit=40&local=true",
i.Hostname) i.Hostname)
// FIXME support broken/expired certs
var c = &http.Client{ var c = &http.Client{
Timeout: instanceHTTPTimeout, Timeout: instanceHTTPTimeout,
} }
@ -497,7 +461,7 @@ func (i *Instance) fetchRecentToots() error {
Msgf("got and parsed toots") Msgf("got and parsed toots")
i.registerSuccess() i.registerSuccess()
i.Event("TOOTS_FETCHED") i.Event("TOOTS_FETCHED")
i.bumpFetchSuccess() i.setNextFetchAfter(instanceSpiderInterval)
// this should go fast as either the channel is buffered bigly or the // this should go fast as either the channel is buffered bigly or the
// ingester receives fast and does its own buffering, but run it in its // ingester receives fast and does its own buffering, but run it in its

View File

@ -9,6 +9,7 @@ import (
u "git.eeqj.de/sneak/goutil" u "git.eeqj.de/sneak/goutil"
"github.com/flosch/pongo2" "github.com/flosch/pongo2"
"github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -22,12 +23,7 @@ func (a *Server) instances() []hash {
i := make(hash) i := make(hash)
// TODO move this locking onto a method on Instance that just // TODO move this locking onto a method on Instance that just
// returns a new hash // returns a new hash
// FIXME figure out why a very short lock here deadlocks
//this only locks the FSM, not the whole instance struct
i["status"] = v.Status()
// now do a quick lock of the whole instance just to copy out the
// attrs
v.Lock() v.Lock()
i["hostname"] = v.Hostname i["hostname"] = v.Hostname
i["uuid"] = v.UUID.String() i["uuid"] = v.UUID.String()
@ -35,8 +31,9 @@ func (a *Server) instances() []hash {
i["nextCheckAfter"] = (-1 * now.Sub(v.NextFetch)).String() i["nextCheckAfter"] = (-1 * now.Sub(v.NextFetch)).String()
i["successCount"] = v.SuccessCount i["successCount"] = v.SuccessCount
i["errorCount"] = v.ErrorCount i["errorCount"] = v.ErrorCount
i["consecutiveErrorCount"] = v.ConsecutiveErrorCount
i["identified"] = v.Identified i["identified"] = v.Identified
//this only locks the FSM, not the whole instance struct
i["status"] = v.Status()
i["software"] = "unknown" i["software"] = "unknown"
i["version"] = "unknown" i["version"] = "unknown"
if v.Identified { if v.Identified {
@ -44,7 +41,6 @@ func (a *Server) instances() []hash {
i["version"] = v.ServerVersionString i["version"] = v.ServerVersionString
} }
v.Unlock() v.Unlock()
resp = append(resp, i) resp = append(resp, i)
} }
@ -58,7 +54,7 @@ func (a *Server) instances() []hash {
return resp return resp
} }
func (a *Server) instanceStatusSummary() map[string]int { func (a *Server) instanceSummary() map[string]int {
resp := make(map[string]int) resp := make(map[string]int)
for _, v := range a.feta.manager.ListInstances() { for _, v := range a.feta.manager.ListInstances() {
v.Lock() v.Lock()
@ -72,6 +68,26 @@ func (a *Server) instanceStatusSummary() map[string]int {
return resp return resp
} }
/*
func (a *Server) getInstanceListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
result := &gin.H{
"instances": a.instances(),
}
json, err := json.Marshal(result)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(json)
}
}
*/
func (a *Server) notFoundHandler(c echo.Context) error { func (a *Server) notFoundHandler(c echo.Context) error {
return c.String(http.StatusNotFound, "404 not found") return c.String(http.StatusNotFound, "404 not found")
} }
@ -101,33 +117,17 @@ func (a *Server) instanceHandler(c echo.Context) error {
} }
func (a *Server) indexHandler(c echo.Context) error { func (a *Server) indexHandler(c echo.Context) error {
count, err := a.feta.dbm.TotalTootCount()
if err != nil {
count = 0
}
tc := pongo2.Context{ tc := pongo2.Context{
"time": time.Now().UTC().Format(time.RFC3339Nano), "time": time.Now().UTC().Format(time.RFC3339Nano),
"gitrev": a.feta.version, "gitrev": a.feta.version,
"tootCount": count, "instances": a.instances(),
"instances": a.instances(),
"instanceStatusSummary": a.instanceStatusSummary(),
} }
return c.Render(http.StatusOK, "index.html", tc) return c.Render(http.StatusOK, "index.html", tc)
} }
func (a *Server) instanceListHandler(c echo.Context) error {
il := a.instances()
tc := pongo2.Context{
"time": time.Now().UTC().Format(time.RFC3339Nano),
"gitrev": a.feta.version,
"instances": il,
}
return c.Render(http.StatusOK, "instancelist.html", tc)
}
func (a *Server) statsHandler(c echo.Context) error { func (a *Server) statsHandler(c echo.Context) error {
index := &hash{ index := &gin.H{
"server": &hash{ "server": &gin.H{
"now": time.Now().UTC().Format(time.RFC3339), "now": time.Now().UTC().Format(time.RFC3339),
"uptime": a.feta.uptime().String(), "uptime": a.feta.uptime().String(),
"goroutines": runtime.NumGoroutine(), "goroutines": runtime.NumGoroutine(),
@ -135,14 +135,14 @@ func (a *Server) statsHandler(c echo.Context) error {
"version": a.feta.version, "version": a.feta.version,
"buildarch": a.feta.buildarch, "buildarch": a.feta.buildarch,
}, },
"instanceStatusSummary": a.instanceStatusSummary(), "instanceSummary": a.instanceSummary(),
} }
return c.JSONPretty(http.StatusOK, index, " ") return c.JSONPretty(http.StatusOK, index, " ")
} }
func (a *Server) healthCheckHandler(c echo.Context) error { func (a *Server) healthCheckHandler(c echo.Context) error {
resp := &hash{ resp := &gin.H{
"status": "ok", "status": "ok",
"now": time.Now().UTC().Format(time.RFC3339), "now": time.Now().UTC().Format(time.RFC3339),
"uptime": a.feta.uptime().String(), "uptime": a.feta.uptime().String(),

View File

@ -96,7 +96,6 @@ func (s *Server) initRouter() {
// Routes // Routes
s.e.GET("/", s.indexHandler) s.e.GET("/", s.indexHandler)
s.e.GET("/instance/:uuid", s.instanceHandler) s.e.GET("/instance/:uuid", s.instanceHandler)
s.e.GET("/instances", s.instanceListHandler)
s.e.GET("/stats.json", s.statsHandler) s.e.GET("/stats.json", s.statsHandler)
s.e.GET("/.well-known/healthcheck.json", s.healthCheckHandler) s.e.GET("/.well-known/healthcheck.json", s.healthCheckHandler)
//a.e.GET("/about", s.aboutHandler) //a.e.GET("/about", s.aboutHandler)

View File

@ -4,44 +4,34 @@
<div class="col-lg-12"> <div class="col-lg-12">
<h2>feta overview</h2> <h2>indexer stats</h2>
<div class="card m-5">
<h5 class="card-header">Instances</h5>
<div class="card-body">
<h5 class="card-title">Tracking {{ instances | length }} instances
across the Fediverse.</h5>
<!--
<p class="card-text">
</p> -->
<a href="/instances" class="btn btn-primary">View Instance List</a>
</div>
</div>
<div class="card m-5">
<h5 class="card-header">Toots</h5>
<div class="card-body">
<h5 class="card-title">I have {{ tootCount }} toots
in my database.</h5>
<a href="/toots" class="btn btn-primary">View Latest Toots</a>
</div>
</div>
<div class="card m-5">
<h5 class="card-header">Recent Events</h5>
<div class="card-body">
<h5 class="card-title">Last n System Events</h5>
<p class="card-text"> Discovered instance toot1.example.com </p>
<p class="card-text"> Discovered instance toot2.example.com </p>
<p class="card-text"> Discovered instance toot3.example.com </p>
<p class="card-text"> Discovered instance toot4.example.com </p>
</div>
</div>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">instance id</th>
<th scope="col">hostname</th>
<th scope="col">status</th>
<th scope="col">tootCount</th>
<th scope="col">Detail</th>
</tr>
</thead>
<tbody>
{% for instance in instances %}
<tr>
<td><a href="/instance/{{instance.uuid}}">{{instance.uuid}}</a></td>
<td><a href="https://{{instance.hostname}}">{{instance.hostname}}</a></td>
<td>{{instance.status}}</td>
<td>{{instance.tootCount}}</td>
<td><a
href="/instance/{{instance.uuid}}"
class="btn btn-info">
<i class="fab fa-mastodon"></i>
</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,24 +0,0 @@
{% extends "page.html" %}
{% block content %}
<div class="col-lg-12">
<h2>instance {{instance.hostname}}</h2>
<div class="card m-5">
<div class="card-header">
<a href="/instance/{{instance.uuid}}">{{ instance.hostname }}</a>
({{instance.tootCount}} toots)
</div>
<div class="card-body">
<h5 class="card-title">{{instance.status}}</h5>
<p class="card-text">First Stat</p>
<p class="card-text">Second Stat</p>
<p class="card-text">Third Stat</p>
<a href="https://{{instance.hostname}}" class="btn btn-primary">View Instance Website</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,36 +0,0 @@
{% extends "page.html" %}
{% block content %}
<div class="col-lg-12">
<h2>instance list</h2>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">hostname</th>
<th scope="col">status</th>
<th scope="col">tootCount</th>
<th scope="col">nextFetch</th>
<th scope="col">Detail</th>
</tr>
</thead>
<tbody>
{% for instance in instances %}
<tr>
<td><a href="https://{{instance.hostname}}">{{instance.hostname}}</a></td>
<td>{{instance.status}}</td>
<td>{{instance.tootCount}}</td>
<td>{{instance.nextFetch}}</td>
<td><a
href="/instance/{{instance.uuid}}"
class="btn btn-info">
<i class="fab fa-mastodon"></i>
</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}