Commit 57211c6d authored by refcell's avatar refcell Committed by GitHub

fix(op-dispute-mon): Improve Resolution Status Granularity (#10419)

* stash resolution status

* fix(op-dispute-mon): resolution status metric

* fix(op-dispute-mon): add logs for resolvable games
parent 4386680d
...@@ -19,6 +19,22 @@ import ( ...@@ -19,6 +19,22 @@ import (
const Namespace = "op_dispute_mon" const Namespace = "op_dispute_mon"
type ResolutionStatus uint8
const (
// In progress
CompleteMaxDuration ResolutionStatus = iota
CompleteBeforeMaxDuration
// Resolvable
ResolvableMaxDuration
ResolvableBeforeMaxDuration
// Not resolvable
InProgressMaxDuration
InProgressBeforeMaxDuration
)
type CreditExpectation uint8 type CreditExpectation uint8
const ( const (
...@@ -93,7 +109,7 @@ type Metricer interface { ...@@ -93,7 +109,7 @@ type Metricer interface {
RecordHonestActorClaims(address common.Address, stats *HonestActorData) RecordHonestActorClaims(address common.Address, stats *HonestActorData)
RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int) RecordGameResolutionStatus(status ResolutionStatus, count int)
RecordCredit(expectation CreditExpectation, count int) RecordCredit(expectation CreditExpectation, count int)
...@@ -311,16 +327,26 @@ func (m *Metrics) RecordHonestActorClaims(address common.Address, stats *HonestA ...@@ -311,16 +327,26 @@ func (m *Metrics) RecordHonestActorClaims(address common.Address, stats *HonestA
m.honestActorBonds.WithLabelValues(address.Hex(), "won").Set(weiToEther(stats.WonBonds)) m.honestActorBonds.WithLabelValues(address.Hex(), "won").Set(weiToEther(stats.WonBonds))
} }
func (m *Metrics) RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int) { func (m *Metrics) RecordGameResolutionStatus(status ResolutionStatus, count int) {
completion := "complete" asLabels := func(status ResolutionStatus) []string {
if !complete { switch status {
completion = "in_progress" case CompleteMaxDuration:
return []string{"complete", "max_duration"}
case CompleteBeforeMaxDuration:
return []string{"complete", "before_max_duration"}
case ResolvableMaxDuration:
return []string{"resolvable", "max_duration"}
case ResolvableBeforeMaxDuration:
return []string{"resolvable", "before_max_duration"}
case InProgressMaxDuration:
return []string{"in_progress", "max_duration"}
case InProgressBeforeMaxDuration:
return []string{"in_progress", "before_max_duration"}
default:
panic(fmt.Errorf("unknown resolution status: %v", status))
} }
maxDuration := "reached"
if !maxDurationReached {
maxDuration = "not_reached"
} }
m.resolutionStatus.WithLabelValues(completion, maxDuration).Set(float64(count)) m.resolutionStatus.WithLabelValues(asLabels(status)...).Set(float64(count))
} }
func (m *Metrics) RecordCredit(expectation CreditExpectation, count int) { func (m *Metrics) RecordCredit(expectation CreditExpectation, count int) {
......
...@@ -21,7 +21,7 @@ func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {} ...@@ -21,7 +21,7 @@ func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {}
func (*NoopMetricsImpl) RecordHonestActorClaims(_ common.Address, _ *HonestActorData) {} func (*NoopMetricsImpl) RecordHonestActorClaims(_ common.Address, _ *HonestActorData) {}
func (*NoopMetricsImpl) RecordGameResolutionStatus(_ bool, _ bool, _ int) {} func (*NoopMetricsImpl) RecordGameResolutionStatus(_ ResolutionStatus, _ int) {}
func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {} func (*NoopMetricsImpl) RecordCredit(_ CreditExpectation, _ int) {}
......
package mon package mon
import ( import (
"time"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-dispute-mon/metrics"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
const MaxResolveDelay = time.Minute
type ResolutionMetrics interface { type ResolutionMetrics interface {
RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int) RecordGameResolutionStatus(status metrics.ResolutionStatus, count int)
} }
type ResolutionMonitor struct { type ResolutionMonitor struct {
...@@ -24,39 +29,48 @@ func NewResolutionMonitor(logger log.Logger, metrics ResolutionMetrics, clock RC ...@@ -24,39 +29,48 @@ func NewResolutionMonitor(logger log.Logger, metrics ResolutionMetrics, clock RC
} }
} }
type resolutionStatus struct { func (r *ResolutionMonitor) CheckResolutions(games []*types.EnrichedGameData) {
completeMaxDuration int statusMetrics := make(map[metrics.ResolutionStatus]int)
completeBeforeMaxDuration int for _, game := range games {
inProgressMaxDuration int complete := game.Status != gameTypes.GameStatusInProgress
inProgressBeforeMaxDuration int duration := uint64(r.clock.Now().Unix()) - game.Timestamp
} maxDurationReached := duration >= (2 * game.MaxClockDuration)
resolvable := true
func (r *resolutionStatus) Inc(complete, maxDuration bool) { for _, claim := range game.Claims {
// If any claim is not resolved, the game is not resolvable
resolvable = resolvable && claim.Resolved
}
if complete { if complete {
if maxDuration { if maxDurationReached {
r.completeMaxDuration++ statusMetrics[metrics.CompleteMaxDuration]++
} else { } else {
r.completeBeforeMaxDuration++ statusMetrics[metrics.CompleteBeforeMaxDuration]++
}
} else if resolvable {
if maxDurationReached {
// SAFETY: since maxDurationReached is true, this cannot underflow
delay := duration - (2 * game.MaxClockDuration)
if delay > uint64(MaxResolveDelay.Seconds()) {
r.logger.Warn("Resolvable game has taken too long to resolve", "game", game.Proxy, "delay", delay)
} }
statusMetrics[metrics.ResolvableMaxDuration]++
} else { } else {
if maxDuration { statusMetrics[metrics.ResolvableBeforeMaxDuration]++
r.inProgressMaxDuration++ }
} else {
if maxDurationReached {
// Note: we don't need to log here since unresolved claims are logged and metriced in claims.go
statusMetrics[metrics.InProgressMaxDuration]++
} else { } else {
r.inProgressBeforeMaxDuration++ statusMetrics[metrics.InProgressBeforeMaxDuration]++
} }
} }
}
func (r *ResolutionMonitor) CheckResolutions(games []*types.EnrichedGameData) {
status := &resolutionStatus{}
for _, game := range games {
complete := game.Status != gameTypes.GameStatusInProgress
duration := uint64(r.clock.Now().Unix()) - game.Timestamp
maxDurationReached := duration >= game.MaxClockDuration
status.Inc(complete, maxDurationReached)
} }
r.metrics.RecordGameResolutionStatus(true, true, status.completeMaxDuration)
r.metrics.RecordGameResolutionStatus(true, false, status.completeBeforeMaxDuration) r.metrics.RecordGameResolutionStatus(metrics.CompleteMaxDuration, statusMetrics[metrics.CompleteMaxDuration])
r.metrics.RecordGameResolutionStatus(false, true, status.inProgressMaxDuration) r.metrics.RecordGameResolutionStatus(metrics.CompleteBeforeMaxDuration, statusMetrics[metrics.CompleteBeforeMaxDuration])
r.metrics.RecordGameResolutionStatus(false, false, status.inProgressBeforeMaxDuration) r.metrics.RecordGameResolutionStatus(metrics.ResolvableMaxDuration, statusMetrics[metrics.ResolvableMaxDuration])
r.metrics.RecordGameResolutionStatus(metrics.ResolvableBeforeMaxDuration, statusMetrics[metrics.ResolvableBeforeMaxDuration])
r.metrics.RecordGameResolutionStatus(metrics.InProgressMaxDuration, statusMetrics[metrics.InProgressMaxDuration])
r.metrics.RecordGameResolutionStatus(metrics.InProgressBeforeMaxDuration, statusMetrics[metrics.InProgressBeforeMaxDuration])
} }
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"time" "time"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-dispute-mon/metrics"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
...@@ -17,10 +18,12 @@ func TestResolutionMonitor_CheckResolutions(t *testing.T) { ...@@ -17,10 +18,12 @@ func TestResolutionMonitor_CheckResolutions(t *testing.T) {
games := newTestGames(uint64(cl.Now().Unix())) games := newTestGames(uint64(cl.Now().Unix()))
r.CheckResolutions(games) r.CheckResolutions(games)
require.Equal(t, 1, m.calls[true][true]) require.Equal(t, 1, m.calls[metrics.CompleteMaxDuration])
require.Equal(t, 1, m.calls[true][false]) require.Equal(t, 1, m.calls[metrics.CompleteBeforeMaxDuration])
require.Equal(t, 1, m.calls[false][true]) require.Equal(t, 1, m.calls[metrics.ResolvableMaxDuration])
require.Equal(t, 1, m.calls[false][false]) require.Equal(t, 1, m.calls[metrics.ResolvableBeforeMaxDuration])
require.Equal(t, 1, m.calls[metrics.InProgressMaxDuration])
require.Equal(t, 1, m.calls[metrics.InProgressBeforeMaxDuration])
} }
func newTestResolutionMonitor(t *testing.T) (*ResolutionMonitor, *clock.DeterministicClock, *stubResolutionMetrics) { func newTestResolutionMonitor(t *testing.T) (*ResolutionMonitor, *clock.DeterministicClock, *stubResolutionMetrics) {
...@@ -31,26 +34,37 @@ func newTestResolutionMonitor(t *testing.T) (*ResolutionMonitor, *clock.Determin ...@@ -31,26 +34,37 @@ func newTestResolutionMonitor(t *testing.T) (*ResolutionMonitor, *clock.Determin
} }
type stubResolutionMetrics struct { type stubResolutionMetrics struct {
calls map[bool]map[bool]int // completed -> max duration reached -> call count calls map[metrics.ResolutionStatus]int
} }
func (s *stubResolutionMetrics) RecordGameResolutionStatus(complete bool, maxDurationReached bool, count int) { func (s *stubResolutionMetrics) RecordGameResolutionStatus(status metrics.ResolutionStatus, count int) {
if s.calls == nil { if s.calls == nil {
s.calls = make(map[bool]map[bool]int) s.calls = make(map[metrics.ResolutionStatus]int)
s.calls[true] = make(map[bool]int)
s.calls[false] = make(map[bool]int)
} }
s.calls[complete][maxDurationReached] += count s.calls[status] += count
} }
func newTestGames(duration uint64) []*types.EnrichedGameData { func newTestGames(duration uint64) []*types.EnrichedGameData {
newTestGame := func(duration uint64, status gameTypes.GameStatus) *types.EnrichedGameData { newTestGame := func(duration uint64, status gameTypes.GameStatus, resolvable bool) *types.EnrichedGameData {
return &types.EnrichedGameData{MaxClockDuration: duration, Status: status} game := &types.EnrichedGameData{
MaxClockDuration: duration,
Status: status,
}
if !resolvable {
game.Claims = []types.EnrichedClaim{
{
Resolved: false,
},
}
}
return game
} }
return []*types.EnrichedGameData{ return []*types.EnrichedGameData{
newTestGame(duration, gameTypes.GameStatusInProgress), newTestGame(duration/2, gameTypes.GameStatusInProgress, false),
newTestGame(duration*10, gameTypes.GameStatusInProgress), newTestGame(duration*5, gameTypes.GameStatusInProgress, false),
newTestGame(duration, gameTypes.GameStatusDefenderWon), newTestGame(duration/2, gameTypes.GameStatusInProgress, true),
newTestGame(duration*10, gameTypes.GameStatusChallengerWon), newTestGame(duration*5, gameTypes.GameStatusInProgress, true),
newTestGame(duration/2, gameTypes.GameStatusDefenderWon, false),
newTestGame(duration*5, gameTypes.GameStatusChallengerWon, false),
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment