feta/instance/instance.go

514 lines
13 KiB
Go
Raw Normal View History

2019-12-19 14:24:26 +00:00
package instance
2019-10-24 10:38:16 +00:00
2020-03-27 23:02:36 +00:00
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
"git.eeqj.de/sneak/feta/jsonapis"
"git.eeqj.de/sneak/feta/toot"
"github.com/google/uuid"
"github.com/looplab/fsm"
"github.com/rs/zerolog/log"
)
2019-11-02 06:56:17 +00:00
const nodeInfoSchemaVersionTwoName = "http://nodeinfo.diaspora.software/ns/schema/2.0"
2020-04-09 07:38:19 +00:00
const instanceNodeinfoTimeout = time.Second * 60 * 2 // 2m
const instanceHTTPTimeout = time.Second * 60 * 2 // 2m
const instanceSpiderInterval = time.Second * 60 * 2 // 2m
const instanceErrorInterval = time.Second * 60 * 60 // 1h
const instancePersistentErrorInterval = time.Second * 86400 // 1d
const zeroInterval = time.Second * 0 // 0s
2019-12-19 14:24:26 +00:00
// Instance stores all the information we know about an instance
type Instance struct {
2020-03-28 01:17:52 +00:00
Disabled bool
2019-12-19 14:24:26 +00:00
ErrorCount uint
2020-04-09 07:38:19 +00:00
ConsecutiveErrorCount uint
2020-03-28 01:17:52 +00:00
FSM *fsm.FSM
Fetching bool
HighestID uint
2019-12-19 14:24:26 +00:00
Hostname string
Identified bool
2020-03-28 02:57:58 +00:00
Implementation string
InitialFSMState string
2019-12-19 14:24:26 +00:00
NextFetch time.Time
2020-04-05 01:16:37 +00:00
LastError string
2020-03-28 01:17:52 +00:00
NodeInfoURL string
2019-12-19 14:24:26 +00:00
ServerImplementationString string
2020-03-28 01:17:52 +00:00
ServerVersionString string
SuccessCount uint
2020-03-28 02:57:58 +00:00
UUID uuid.UUID
2019-11-05 23:32:09 +00:00
fetchingLock sync.Mutex
2019-11-06 07:03:42 +00:00
fsmLock sync.Mutex
2020-03-28 01:17:52 +00:00
structLock sync.Mutex
tootDestination chan *toot.Toot
2019-10-24 10:38:16 +00:00
}
2019-12-19 14:24:26 +00:00
// New returns a new instance, argument is a function that operates on the
// new instance
func New(options ...func(i *Instance)) *Instance {
i := new(Instance)
2020-03-28 01:17:52 +00:00
i.UUID = uuid.New()
2019-11-06 07:03:42 +00:00
i.setNextFetchAfter(1 * time.Second)
2020-03-28 02:57:58 +00:00
i.InitialFSMState = "STATUS_UNKNOWN"
for _, opt := range options {
opt(i)
}
2019-11-06 07:03:42 +00:00
if i.InitialFSMState == "FETCHING" {
i.InitialFSMState = "READY_FOR_TOOTFETCH"
}
2020-03-28 01:17:52 +00:00
i.FSM = fsm.NewFSM(
2020-03-28 02:57:58 +00:00
i.InitialFSMState,
2019-11-06 07:03:42 +00:00
fsm.Events{
{Name: "BEGIN_NODEINFO_URL_FETCH", Src: []string{"STATUS_UNKNOWN"}, Dst: "FETCHING_NODEINFO_URL"},
{Name: "GOT_NODEINFO_URL", Src: []string{"FETCHING_NODEINFO_URL"}, Dst: "PRE_NODEINFO_FETCH"},
{Name: "BEGIN_NODEINFO_FETCH", Src: []string{"PRE_NODEINFO_FETCH"}, Dst: "FETCHING_NODEINFO"},
{Name: "GOT_NODEINFO", Src: []string{"FETCHING_NODEINFO"}, Dst: "READY_FOR_TOOTFETCH"},
{Name: "FETCH_TIME_REACHED", Src: []string{"READY_FOR_TOOTFETCH"}, Dst: "READY_AND_DUE_FETCH"},
2019-12-14 16:34:13 +00:00
{Name: "BEGIN_TOOT_FETCH", Src: []string{"READY_AND_DUE_FETCH"}, Dst: "FETCHING"},
2019-11-06 07:03:42 +00:00
{Name: "WEIRD_NODE_RESPONSE", Src: []string{"FETCHING_NODEINFO_URL", "PRE_NODEINFO_FETCH", "FETCHING_NODEINFO"}, Dst: "WEIRD_NODE"},
{Name: "EARLY_FETCH_ERROR", Src: []string{"FETCHING_NODEINFO_URL", "PRE_NODEINFO_FETCH", "FETCHING_NODEINFO"}, Dst: "EARLY_ERROR"},
2019-12-14 16:34:13 +00:00
{Name: "TOOT_FETCH_ERROR", Src: []string{"FETCHING"}, Dst: "TOOT_FETCH_ERROR"},
{Name: "TOOTS_FETCHED", Src: []string{"FETCHING"}, Dst: "READY_FOR_TOOTFETCH"},
{Name: "DISABLEMENT", Src: []string{"WEIRD_NODE", "EARLY_ERROR", "TOOT_FETCH_ERROR"}, Dst: "DISABLED"},
2019-11-06 07:03:42 +00:00
},
fsm.Callbacks{
"enter_state": func(e *fsm.Event) { i.fsmEnterState(e) },
},
)
2019-11-06 07:03:42 +00:00
return i
2019-10-24 11:56:44 +00:00
}
2019-12-19 14:24:26 +00:00
// Status returns the instance's state in the FSM
func (i *Instance) Status() string {
2019-11-06 07:03:42 +00:00
i.fsmLock.Lock()
defer i.fsmLock.Unlock()
2020-03-28 01:17:52 +00:00
return i.FSM.Current()
2019-11-06 00:46:52 +00:00
}
2019-12-19 14:24:26 +00:00
// SetTootDestination takes a channel from the manager that all toots
// fetched from this instance should be pushed into. The instance is not
// responsible for deduplication, it should shove all toots on every fetch
// into the channel.
func (i *Instance) SetTootDestination(d chan *toot.Toot) {
2019-12-14 16:34:13 +00:00
i.tootDestination = d
}
2019-12-19 14:24:26 +00:00
// Event is the method that alters the FSM
func (i *Instance) Event(eventname string) {
2019-11-06 07:03:42 +00:00
i.fsmLock.Lock()
defer i.fsmLock.Unlock()
2020-03-28 01:17:52 +00:00
i.FSM.Event(eventname)
2019-11-06 00:46:52 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) fsmEnterState(e *fsm.Event) {
2019-11-06 07:03:42 +00:00
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-11-06 07:03:42 +00:00
Str("state", e.Dst).
Msg("instance changed state")
2019-11-03 10:56:50 +00:00
}
2019-12-19 14:24:26 +00:00
// Lock locks the instance's mutex for reading/writing from the structure
func (i *Instance) Lock() {
2019-11-06 07:03:42 +00:00
i.structLock.Lock()
2019-11-03 10:56:50 +00:00
}
2019-12-19 14:24:26 +00:00
// Unlock unlocks the instance's mutex for reading/writing from the structure
func (i *Instance) Unlock() {
2019-11-06 07:03:42 +00:00
i.structLock.Unlock()
}
2019-11-05 23:32:09 +00:00
2020-04-09 07:38:19 +00:00
func (i *Instance) bumpFetchError() {
2019-11-06 07:03:42 +00:00
i.Lock()
2020-04-09 07:38:19 +00:00
probablyDead := i.ConsecutiveErrorCount > 3
shouldDisable := i.ConsecutiveErrorCount > 6
2020-04-09 07:38:19 +00:00
i.Unlock()
if shouldDisable {
// auf wiedersehen, felicia
i.Lock()
i.Disabled = true
i.Unlock()
i.Event("DISABLEMENT")
return
}
2020-04-09 07:38:19 +00:00
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)
2019-11-06 07:03:42 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) setNextFetchAfter(d time.Duration) {
2019-11-06 07:03:42 +00:00
i.Lock()
defer i.Unlock()
2019-12-19 14:24:26 +00:00
i.NextFetch = time.Now().Add(d)
2019-11-06 07:03:42 +00:00
}
2019-12-19 14:24:26 +00:00
// Fetch prepares an instance for fetching. Bad name, fix it.
// FIXME(sneak)
func (i *Instance) Fetch() {
2019-11-06 07:03:42 +00:00
i.fetchingLock.Lock()
defer i.fetchingLock.Unlock()
2019-11-06 00:46:52 +00:00
2020-04-09 07:38:19 +00:00
i.bumpFetchError()
2019-12-19 14:24:26 +00:00
err := i.DetectNodeTypeIfNecessary()
2019-11-03 18:00:01 +00:00
if err != nil {
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-11-03 18:00:01 +00:00
Err(err).
Msg("unable to fetch instance metadata")
2019-10-24 11:56:44 +00:00
return
}
2020-04-09 07:38:19 +00:00
i.scheduleFetchImmediate()
2019-12-19 14:24:26 +00:00
log.Info().
Str("hostname", i.Hostname).
Msg("instance now ready for fetch")
2019-11-03 18:00:01 +00:00
}
2019-12-19 14:24:26 +00:00
// FIXME rename this function
func (i *Instance) dueForFetch() bool {
2019-11-06 07:03:42 +00:00
// this just checks FSM state, the ticker must update it and do time
// calcs
if i.Status() == "READY_AND_DUE_FETCH" {
return true
}
return false
}
2019-12-19 14:24:26 +00:00
func (i *Instance) isNowPastFetchTime() bool {
return time.Now().After(i.NextFetch)
2019-11-06 07:03:42 +00:00
}
2019-12-14 16:34:13 +00:00
// Tick is responsible for pushing idle instance records between states.
// The instances will transition between states when doing stuff (e.g.
// investigating, fetching, et c) as well.
2019-12-19 14:24:26 +00:00
func (i *Instance) Tick() {
2019-11-06 07:03:42 +00:00
if i.Status() == "READY_FOR_TOOTFETCH" {
if i.isNowPastFetchTime() {
i.Event("FETCH_TIME_REACHED")
}
} else if i.Status() == "STATUS_UNKNOWN" {
i.Fetch()
2019-12-14 16:34:13 +00:00
} else if i.Status() == "READY_AND_DUE_FETCH" {
i.fetchRecentToots()
2019-11-06 00:46:52 +00:00
}
2019-11-04 17:07:04 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) nodeIdentified() bool {
2019-11-06 07:03:42 +00:00
i.Lock()
defer i.Unlock()
2020-03-28 02:57:58 +00:00
if i.Implementation != "" {
2019-11-04 17:07:04 +00:00
return true
2019-11-03 18:00:01 +00:00
}
2019-11-04 17:07:04 +00:00
return false
2019-11-03 18:00:01 +00:00
}
2019-12-19 14:24:26 +00:00
// DetectNodeTypeIfNecessary does some network requests if the node is as
// yet unidenfitied. No-op otherwise.
func (i *Instance) DetectNodeTypeIfNecessary() error {
2019-11-06 07:03:42 +00:00
if !i.nodeIdentified() {
return i.fetchNodeInfo()
2019-12-14 15:49:35 +00:00
}
return nil
2019-10-24 11:56:44 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) registerError() {
2019-11-06 07:03:42 +00:00
i.Lock()
defer i.Unlock()
2019-12-19 14:24:26 +00:00
i.ErrorCount++
2020-04-09 07:38:19 +00:00
i.ConsecutiveErrorCount++
2019-11-04 17:07:04 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) registerSuccess() {
2019-11-06 07:03:42 +00:00
i.Lock()
defer i.Unlock()
2019-12-19 14:24:26 +00:00
i.SuccessCount++
2020-04-09 07:38:19 +00:00
i.ConsecutiveErrorCount = 0
}
2019-12-19 14:24:26 +00:00
// Up returns true if the success count is >0
func (i *Instance) Up() bool {
2019-11-03 18:00:01 +00:00
i.Lock()
defer i.Unlock()
2019-12-19 14:24:26 +00:00
return i.SuccessCount > 0
2019-11-03 18:00:01 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) fetchNodeInfoURL() error {
url := fmt.Sprintf("https://%s/.well-known/nodeinfo", i.Hostname)
2019-10-24 11:56:44 +00:00
var c = &http.Client{
Timeout: instanceNodeinfoTimeout,
2019-10-24 11:56:44 +00:00
}
log.Debug().
Str("url", url).
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Msg("fetching nodeinfo reference URL")
2019-11-06 07:03:42 +00:00
i.Event("BEGIN_NODEINFO_URL_FETCH")
resp, err := c.Get(url)
2019-10-24 11:56:44 +00:00
if err != nil {
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Err(err).
Msg("unable to fetch nodeinfo, node is down?")
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("EARLY_FETCH_ERROR")
2019-11-03 18:00:01 +00:00
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Err(err).
Msg("unable to read nodeinfo")
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("EARLY_FETCH_ERROR")
2019-11-03 18:00:01 +00:00
return err
}
nir := new(jsonapis.NodeInfoWellKnownResponse)
err = json.Unmarshal(body, &nir)
if err != nil {
2019-11-03 18:00:01 +00:00
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Err(err).
Msg("unable to parse nodeinfo, node is weird")
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("WEIRD_NODE_RESPONSE")
2019-11-03 18:00:01 +00:00
return err
}
for _, item := range nir.Links {
if item.Rel == nodeInfoSchemaVersionTwoName {
2019-11-03 18:00:01 +00:00
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Str("nodeinfourl", item.Href).
Msg("success fetching url for nodeinfo")
i.Lock()
2020-03-28 01:17:52 +00:00
i.NodeInfoURL = item.Href
i.Unlock()
i.registerSuccess()
2019-11-06 07:03:42 +00:00
i.Event("GOT_NODEINFO_URL")
2019-11-03 18:00:01 +00:00
return nil
}
2019-11-03 18:00:01 +00:00
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-11-03 18:00:01 +00:00
Str("item-rel", item.Rel).
Str("item-href", item.Href).
2019-11-06 07:03:42 +00:00
Msg("nodeinfo entry")
}
log.Error().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Msg("incomplete nodeinfo")
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("WEIRD_NODE_RESPONSE")
2019-11-03 18:00:01 +00:00
return errors.New("incomplete nodeinfo")
}
2019-12-19 14:24:26 +00:00
func (i *Instance) fetchNodeInfo() error {
2019-11-03 18:00:01 +00:00
err := i.fetchNodeInfoURL()
2019-11-03 10:56:50 +00:00
2019-11-03 18:00:01 +00:00
if err != nil {
return err
2019-10-24 11:56:44 +00:00
}
var c = &http.Client{
Timeout: instanceNodeinfoTimeout,
}
//FIXME make sure the nodeinfourl is on the same domain as the instance
//hostname
2019-11-06 00:46:52 +00:00
i.Lock()
2020-03-28 01:17:52 +00:00
url := i.NodeInfoURL
2019-11-06 00:46:52 +00:00
i.Unlock()
2019-11-03 10:56:50 +00:00
2019-11-06 07:03:42 +00:00
i.Event("BEGIN_NODEINFO_FETCH")
2019-11-03 10:56:50 +00:00
resp, err := c.Get(url)
if err != nil {
2019-11-03 18:00:01 +00:00
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Err(err).
Msgf("unable to fetch nodeinfo data")
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("EARLY_FETCH_ERROR")
2019-11-03 18:00:01 +00:00
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Err(err).
Msgf("unable to read nodeinfo data")
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("EARLY_FETCH_ERROR")
2019-11-03 18:00:01 +00:00
return err
}
ni := new(jsonapis.NodeInfoVersionTwoSchema)
err = json.Unmarshal(body, &ni)
if err != nil {
log.Error().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Err(err).
Msgf("unable to parse nodeinfo")
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("WEIRD_NODE_RESPONSE")
2019-11-03 18:00:01 +00:00
return err
}
2019-11-03 18:00:01 +00:00
log.Debug().
Str("serverVersion", ni.Software.Version).
Str("software", ni.Software.Name).
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2020-03-28 01:17:52 +00:00
Str("nodeInfoURL", i.NodeInfoURL).
Msg("received nodeinfo from instance")
i.Lock()
2019-12-19 14:24:26 +00:00
i.ServerVersionString = ni.Software.Version
i.ServerImplementationString = ni.Software.Name
ni.Software.Name = strings.ToLower(ni.Software.Name)
if ni.Software.Name == "pleroma" {
2019-11-03 18:00:01 +00:00
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Str("software", ni.Software.Name).
Msg("detected server software")
2019-12-19 14:24:26 +00:00
i.Identified = true
2020-03-28 02:57:58 +00:00
i.Implementation = "pleroma"
2019-11-05 23:32:09 +00:00
i.Unlock()
i.registerSuccess()
2019-11-06 07:03:42 +00:00
i.Event("GOT_NODEINFO")
2019-11-03 18:00:01 +00:00
return nil
} else if ni.Software.Name == "mastodon" {
2019-11-06 00:46:52 +00:00
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-11-06 00:46:52 +00:00
Str("software", ni.Software.Name).
Msg("detected server software")
2019-12-19 14:24:26 +00:00
i.Identified = true
2020-03-28 02:57:58 +00:00
i.Implementation = "mastodon"
2019-11-05 23:32:09 +00:00
i.Unlock()
i.registerSuccess()
2019-11-06 07:03:42 +00:00
i.Event("GOT_NODEINFO")
2019-11-03 18:00:01 +00:00
return nil
} else {
log.Error().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
Str("software", ni.Software.Name).
2019-11-03 18:00:01 +00:00
Msg("FIXME unknown server implementation")
2019-11-05 23:32:09 +00:00
i.Unlock()
i.registerError()
2019-11-06 07:03:42 +00:00
i.Event("WEIRD_NODE_RESPONSE")
return errors.New("unknown server implementation")
}
2019-10-24 11:56:44 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) fetchRecentToots() error {
// this would have been about a billion times shorter in python
2019-12-14 16:34:13 +00:00
// it turns out pleroma supports the mastodon api so we'll just use that
// for everything for now
2020-04-09 07:38:19 +00:00
// FIXME would be nice to support non-https
2019-12-14 16:34:13 +00:00
url := fmt.Sprintf("https://%s/api/v1/timelines/public?limit=40&local=true",
2019-12-19 14:24:26 +00:00
i.Hostname)
2019-11-03 10:56:50 +00:00
2020-04-09 07:38:19 +00:00
// FIXME support broken/expired certs
2019-12-14 16:34:13 +00:00
var c = &http.Client{
Timeout: instanceHTTPTimeout,
}
i.Event("BEGIN_TOOT_FETCH")
// we set the interval now to the error interval regardless here as a
// safety against bugs to avoid fetching too frequently by logic
// bug. if the fetch is successful, we will conditionally re-update the
// next fetch to now+successInterval.
i.setNextFetchAfter(instanceErrorInterval)
resp, err := c.Get(url)
if err != nil {
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-12-14 16:34:13 +00:00
Err(err).
Msgf("unable to fetch recent toots")
i.registerError()
i.Event("TOOT_FETCH_ERROR")
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Debug().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-12-14 16:34:13 +00:00
Err(err).
Msgf("unable to read recent toots from response")
i.registerError()
i.Event("TOOT_FETCH_ERROR")
return err
2019-10-24 11:56:44 +00:00
}
2019-12-14 16:34:13 +00:00
2019-12-19 14:24:26 +00:00
tc, err := toot.NewTootCollectionFromMastodonAPIResponse(body, i.Hostname)
2019-12-14 16:34:13 +00:00
if err != nil {
log.Error().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-12-14 16:34:13 +00:00
Err(err).
Msgf("unable to parse recent toot list")
i.registerError()
i.Event("TOOT_FETCH_ERROR")
return err
}
log.Info().
2019-12-19 14:24:26 +00:00
Str("hostname", i.Hostname).
2019-12-19 13:20:23 +00:00
Int("tootCount", len(tc)).
2019-12-14 17:03:38 +00:00
Msgf("got and parsed toots")
i.registerSuccess()
i.Event("TOOTS_FETCHED")
2020-04-09 07:38:19 +00:00
i.bumpFetchSuccess()
2019-12-14 16:34:13 +00:00
2019-12-19 14:24:26 +00:00
// 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
// own goroutine anyway because why not
go i.sendTootsToIngester(tc)
return nil
2019-10-24 11:56:44 +00:00
}
2019-12-19 14:24:26 +00:00
func (i *Instance) sendTootsToIngester(tc []*toot.Toot) {
for _, item := range tc {
i.tootDestination <- item
}
}