<commit_before>package commands

import (
	"errors"
	"github.com/github/git-media/gitmedia"
	"github.com/spf13/cobra"
	"io/ioutil"
	"os"
	"path/filepath"
)

var (
	logsCmd = &cobra.Command{
		Use:   "logs",
		Short: "View error logs",
		Run:   logsCommand,
	}

	logsLastCmd = &cobra.Command{
		Use:   "last",
		Short: "View latest error log",
		Run:   logsLastCommand,
	}

	logsShowCmd = &cobra.Command{
		Use:   "show",
		Short: "View a single error log",
		Run:   logsShowCommand,
	}

	logsClearCmd = &cobra.Command{
		Use:   "clear",
		Short: "Clear all logs",
		Run:   logsClearCommand,
	}

	logsBoomtownCmd = &cobra.Command{
		Use:   "boomtown",
		Short: "Trigger a sample error",
		Run:   logsBoomtownCommand,
	}
)

func logsCommand(cmd *cobra.Command, args []string) {
	for _, path := range sortedLogs() {
		Print(path)
	}
}

func logsLastCommand(cmd *cobra.Command, args []string) {
	logs := sortedLogs()
	if len(logs) < 1 {
		Print("No logs to show")
		return
	}

	logsShowCommand(cmd, logs[len(logs)-1:])
}

func logsShowCommand(cmd *cobra.Command, args []string) {
	if len(args) == 0 {
		Print("Supply a log name.")
		return
	}

	name := args[0]
	by, err := ioutil.ReadFile(filepath.Join(gitmedia.LocalLogDir, name))
	if err != nil {
		Exit("Error reading log: %s", name)
	}

	Debug("Reading log: %s", name)
	os.Stdout.Write(by)
}

func logsClearCommand(cmd *cobra.Command, args []string) {
	err := os.RemoveAll(gitmedia.LocalLogDir)
	if err != nil {
		Panic(err, "Error clearing %s", gitmedia.LocalLogDir)
	}

	Print("Cleared %s", gitmedia.LocalLogDir)
}

func logsBoomtownCommand(cmd *cobra.Command, args []string) {
	Debug("Debug message")
	err := errors.New("Error!")
	Panic(err, "Welcome to Boomtown")
	Debug("Never seen")
}

func sortedLogs() []string {
	fileinfos, err := ioutil.ReadDir(gitmedia.LocalLogDir)
	if err != nil {
		return []string{}
	}

	names := make([]string, len(fileinfos))
	for index, info := range fileinfos {
		names[index] = info.Name()
	}

	return names
}

func init() {
	logsCmd.AddCommand(logsLastCmd, logsShowCmd, logsClearCmd, logsBoomtownCmd)
	RootCmd.AddCommand(logsCmd)
}
<commit_msg>ラララララ ラー ウウウ フフフ<commit_after>package commands

import (
	"errors"
	"github.com/github/git-media/gitmedia"
	"github.com/spf13/cobra"
	"io/ioutil"
	"os"
	"path/filepath"
)

var (
	logsCmd = &cobra.Command{
		Use:   "logs",
		Short: "View error logs",
		Run:   logsCommand,
	}

	logsLastCmd = &cobra.Command{
		Use:   "last",
		Short: "View latest error log",
		Run:   logsLastCommand,
	}

	logsShowCmd = &cobra.Command{
		Use:   "show",
		Short: "View a single error log",
		Run:   logsShowCommand,
	}

	logsClearCmd = &cobra.Command{
		Use:   "clear",
		Short: "Clear all logs",
		Run:   logsClearCommand,
	}

	logsBoomtownCmd = &cobra.Command{
		Use:   "boomtown",
		Short: "Trigger a sample error",
		Run:   logsBoomtownCommand,
	}
)

func logsCommand(cmd *cobra.Command, args []string) {
	for _, path := range sortedLogs() {
		Print(path)
	}
}

func logsLastCommand(cmd *cobra.Command, args []string) {
	logs := sortedLogs()
	if len(logs) < 1 {
		Print("No logs to show")
		return
	}

	logsShowCommand(cmd, logs[len(logs)-1:])
}

func logsShowCommand(cmd *cobra.Command, args []string) {
	if len(args) == 0 {
		Print("Supply a log name.")
		return
	}

	name := args[0]
	by, err := ioutil.ReadFile(filepath.Join(gitmedia.LocalLogDir, name))
	if err != nil {
		Exit("Error reading log: %s", name)
	}

	Debug("Reading log: %s", name)
	os.Stdout.Write(by)
}

func logsClearCommand(cmd *cobra.Command, args []string) {
	err := os.RemoveAll(gitmedia.LocalLogDir)
	if err != nil {
		Panic(err, "Error clearing %s", gitmedia.LocalLogDir)
	}

	Print("Cleared %s", gitmedia.LocalLogDir)
}

func logsBoomtownCommand(cmd *cobra.Command, args []string) {
	Debug("Debug message")
	err := gitmedia.Errorf(errors.New("Inner error message!"), "Error!")
	Panic(err, "Welcome to Boomtown")
	Debug("Never seen")
}

func sortedLogs() []string {
	fileinfos, err := ioutil.ReadDir(gitmedia.LocalLogDir)
	if err != nil {
		return []string{}
	}

	names := make([]string, len(fileinfos))
	for index, info := range fileinfos {
		names[index] = info.Name()
	}

	return names
}

func init() {
	logsCmd.AddCommand(logsLastCmd, logsShowCmd, logsClearCmd, logsBoomtownCmd)
	RootCmd.AddCommand(logsCmd)
}
<|endoftext|>


<commit_before>/*
   Copyright (c) 2014, Percona LLC and/or its affiliates. All rights reserved.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package monitor

import (
	"github.com/percona/percona-agent/pct"
	"sync"
)

type Subscribers struct {
	logger *pct.Logger
	// --
	subscribers map[chan bool]bool
	sync.RWMutex
}

func NewSubscribers(logger *pct.Logger) *Subscribers {
	return &Subscribers{
		logger:      logger,
		subscribers: make(map[chan bool]bool),
	}
}

func (s *Subscribers) Add() (c chan bool) {
	s.Lock()
	defer s.Unlock()

	c = make(chan bool, 5)
	s.subscribers[c] = true

	return c
}

func (s *Subscribers) Remove(c chan bool) {
	s.Lock()
	defer s.Unlock()

	if s.subscribers[c] {
		delete(s.subscribers, c)
	}
}

func (s *Subscribers) Empty() bool {
	s.RLock()
	defer s.RUnlock()

	return len(s.subscribers) == 0
}

func (s *Subscribers) Notify() {
	s.RLock()
	defer s.RUnlock()

	for c, _ := range s.subscribers {
		select {
		case c <- true:
		default:
			s.logger.Warn("Unable to notify subscriber")
		}
	}
}
<commit_msg>PCT-637: use buffer 1 for listener chan; timeout 1s instead of 0s; See dicussion: https://github.com/percona/percona-agent/pull/59#discussion_r14302553<commit_after>/*
   Copyright (c) 2014, Percona LLC and/or its affiliates. All rights reserved.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package monitor

import (
	"github.com/percona/percona-agent/pct"
	"sync"
	"time"
)

type Subscribers struct {
	logger *pct.Logger
	// --
	subscribers map[chan bool]bool
	sync.RWMutex
}

func NewSubscribers(logger *pct.Logger) *Subscribers {
	return &Subscribers{
		logger:      logger,
		subscribers: make(map[chan bool]bool),
	}
}

func (s *Subscribers) Add() (c chan bool) {
	s.Lock()
	defer s.Unlock()

	c = make(chan bool, 1)
	s.subscribers[c] = true

	return c
}

func (s *Subscribers) Remove(c chan bool) {
	s.Lock()
	defer s.Unlock()

	if s.subscribers[c] {
		delete(s.subscribers, c)
	}
}

func (s *Subscribers) Empty() bool {
	s.RLock()
	defer s.RUnlock()

	return len(s.subscribers) == 0
}

func (s *Subscribers) Notify() {
	s.RLock()
	defer s.RUnlock()

	for c, _ := range s.subscribers {
		select {
		case c <- true:
		case <-time.After(1 * time.Second):
			s.logger.Warn("Unable to notify subscriber")
		}
	}
}
<|endoftext|>


<commit_before>// (c) 2021, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package throttling

import (
	"sync"
	"time"

	"github.com/ava-labs/avalanchego/ids"
	"github.com/ava-labs/avalanchego/snow/validators"
	"github.com/ava-labs/avalanchego/utils/constants"
	"github.com/ava-labs/avalanchego/utils/logging"
	"github.com/ava-labs/avalanchego/utils/metric"
	"github.com/ava-labs/avalanchego/utils/wrappers"
	"github.com/prometheus/client_golang/prometheus"
)

var (
	_ MsgThrottler = &noMsgThrottler{}
	_ MsgThrottler = &sybilMsgThrottler{}
)

// MsgThrottler rate-limits incoming messages from the network.
type MsgThrottler interface {
	// Blocks until node [nodeID] can put a message of
	// size [msgSize] onto the incoming message buffer.
	Acquire(msgSize uint64, nodeID ids.ShortID)

	// Mark that a message from [nodeID] of size [msgSize]
	// has been removed from the incoming message buffer.
	Release(msgSize uint64, nodeID ids.ShortID)
}

// See sybilMsgThrottler
type MsgThrottlerConfig struct {
	VdrAllocSize        uint64
	AtLargeAllocSize    uint64
	NodeMaxAtLargeBytes uint64
}

// Returns a new MsgThrottler.
// If this function returns an error, the returned MsgThrottler may still be used.
// However, some of its metrics may not be registered.
func NewSybilMsgThrottler(
	log logging.Logger,
	metricsRegisterer prometheus.Registerer,
	vdrs validators.Set,
	config MsgThrottlerConfig,
) (MsgThrottler, error) {
	t := &sybilMsgThrottler{
		log:                    log,
		cond:                   sync.NewCond(&sync.Mutex{}),
		vdrs:                   vdrs,
		maxVdrBytes:            config.VdrAllocSize,
		remainingVdrBytes:      config.VdrAllocSize,
		remainingAtLargeBytes:  config.AtLargeAllocSize,
		nodeMaxAtLargeBytes:    config.NodeMaxAtLargeBytes,
		nodeToVdrBytesUsed:     make(map[ids.ShortID]uint64),
		nodeToAtLargeBytesUsed: make(map[ids.ShortID]uint64),
	}
	if err := t.metrics.initialize(metricsRegisterer); err != nil {
		return nil, err
	}
	return t, nil
}

// msgThrottler implements MsgThrottler.
// It gives more space to validators with more stake.
type sybilMsgThrottler struct {
	log     logging.Logger
	metrics sybilMsgThrottlerMetrics
	cond    *sync.Cond
	// Primary network validator set
	vdrs validators.Set
	// Max number of unprocessed bytes from validators
	maxVdrBytes uint64
	// Max number of bytes that can be taken from the
	// at-large byte allocation by a given node.
	nodeMaxAtLargeBytes uint64
	// Number of bytes left in the validator byte allocation.
	// Initialized to [maxVdrBytes].
	remainingVdrBytes uint64
	// Number of bytes left in the at-large byte allocation
	remainingAtLargeBytes uint64
	// Node ID --> Bytes they've taken from the validator allocation
	nodeToVdrBytesUsed map[ids.ShortID]uint64
	// Node ID --> Bytes they've taken from the at-large allocation
	nodeToAtLargeBytesUsed map[ids.ShortID]uint64
}

// Returns when we can read a message of size [msgSize] from node [nodeID].
// Release([msgSize], [nodeID]) must be called (!) when done with the message
// or when we give up trying to read the message, if applicable.
func (t *sybilMsgThrottler) Acquire(msgSize uint64, nodeID ids.ShortID) {
	t.cond.L.Lock()
	defer t.cond.L.Unlock()

	t.metrics.awaitingAcquire.Inc()
	startTime := time.Now()

	for { // [t.cond.L] is held while in this loop
		atLargeBytesUsed := t.nodeToAtLargeBytesUsed[nodeID]
		// See if we can take from the at-large byte allocation
		if msgSize <= t.remainingAtLargeBytes && atLargeBytesUsed+msgSize <= t.nodeMaxAtLargeBytes {
			// Take from the at-large byte allocation
			t.remainingAtLargeBytes -= msgSize
			t.nodeToAtLargeBytesUsed[nodeID] += msgSize
			break
		}

		// See if we can use the validator byte allocation
		weight, isVdr := t.vdrs.GetWeight(nodeID)
		if !isVdr {
			// This node isn't a validator.
			// Wait until there are more bytes in an allocation.
			t.cond.Wait()
			continue
		}

		// From the at-large allocation, take all the bytes we can
		// without exceeding the per-node limit on taking from it
		atLargeBytesToUse := t.nodeMaxAtLargeBytes - atLargeBytesUsed
		if atLargeBytesToUse > t.remainingAtLargeBytes {
			atLargeBytesToUse = t.remainingAtLargeBytes
		}
		// Need [vdrBytesNeeded] from the validator allocation.
		vdrBytesNeeded := msgSize - atLargeBytesToUse
		if t.remainingVdrBytes < vdrBytesNeeded {
			// Wait until there are more bytes in an allocation.
			t.cond.Wait()
			continue
		}

		// Number of bytes this node can take from validator allocation.
		vdrBytesAllowed := uint64(0)
		// [totalVdrWeight] should always be > 0 but handle this case
		// for completeness to prevent divide by 0
		totalVdrWeight := t.vdrs.Weight()
		if totalVdrWeight != 0 {
			vdrBytesAllowed = uint64(float64(t.maxVdrBytes) * float64(weight) / float64(totalVdrWeight))
		} else {
			t.log.Warn("total validator weight is 0") // this should never happen
		}
		if t.nodeToVdrBytesUsed[nodeID]+vdrBytesNeeded > vdrBytesAllowed {
			// Wait until there are more bytes in an allocation.
			t.cond.Wait()
			continue
		}

		// Use some of [remainingAtLargeBytes] and some of [remainingVdrBytes]
		t.remainingVdrBytes -= vdrBytesNeeded
		if atLargeBytesToUse != 0 {
			t.nodeToAtLargeBytesUsed[nodeID] += atLargeBytesToUse
		}
		t.remainingAtLargeBytes -= atLargeBytesToUse
		t.nodeToVdrBytesUsed[nodeID] += vdrBytesNeeded
		break
	}
	t.metrics.acquireLatency.Observe(float64(time.Since(startTime)))
	t.metrics.remainingAtLargeBytes.Set(float64(t.remainingAtLargeBytes))
	t.metrics.remainingVdrBytes.Set(float64(t.remainingVdrBytes))
	t.metrics.awaitingAcquire.Dec()
	t.metrics.awaitingRelease.Inc()
}

// Must correspond to a previous call of Acquire([msgSize], [nodeID])
func (t *sybilMsgThrottler) Release(msgSize uint64, nodeID ids.ShortID) {
	t.cond.L.Lock()
	defer t.cond.L.Unlock()

	// Try to release these bytes back to the validator allocation
	vdrBytesUsed := t.nodeToVdrBytesUsed[nodeID]
	switch { // This switch is exhaustive
	case vdrBytesUsed > msgSize:
		// Put all bytes back in validator allocation
		t.remainingVdrBytes += msgSize
		t.nodeToVdrBytesUsed[nodeID] -= msgSize
	case vdrBytesUsed == msgSize:
		// Put all bytes back in validator allocation
		t.remainingVdrBytes += msgSize
		delete(t.nodeToVdrBytesUsed, nodeID)
	case vdrBytesUsed < msgSize && vdrBytesUsed > 0:
		// Put some bytes back in validator allocation
		t.remainingVdrBytes += vdrBytesUsed
		t.remainingAtLargeBytes += msgSize - vdrBytesUsed
		t.nodeToAtLargeBytesUsed[nodeID] -= msgSize - vdrBytesUsed
		if t.nodeToAtLargeBytesUsed[nodeID] == 0 {
			delete(t.nodeToAtLargeBytesUsed, nodeID)
		}
		delete(t.nodeToVdrBytesUsed, nodeID)
	case vdrBytesUsed < msgSize && vdrBytesUsed == 0:
		// Put no bytes in validator allocation
		t.remainingAtLargeBytes += msgSize
		t.nodeToAtLargeBytesUsed[nodeID] -= msgSize
		if t.nodeToAtLargeBytesUsed[nodeID] == 0 {
			delete(t.nodeToAtLargeBytesUsed, nodeID)
		}
	}

	t.metrics.remainingAtLargeBytes.Set(float64(t.remainingAtLargeBytes))
	t.metrics.remainingVdrBytes.Set(float64(t.remainingVdrBytes))
	t.metrics.awaitingRelease.Dec()

	// Notify that there are more bytes available
	t.cond.Broadcast()
}

type sybilMsgThrottlerMetrics struct {
	acquireLatency        prometheus.Histogram
	remainingAtLargeBytes prometheus.Gauge
	remainingVdrBytes     prometheus.Gauge
	awaitingAcquire       prometheus.Gauge
	awaitingRelease       prometheus.Gauge
}

func (m *sybilMsgThrottlerMetrics) initialize(metricsRegisterer prometheus.Registerer) error {
	m.acquireLatency = prometheus.NewHistogram(prometheus.HistogramOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_acquire_latency",
		Help:      "Duration an incoming message waited to be read due to throttling",
		Buckets:   metric.NanosecondsBuckets,
	})
	m.remainingAtLargeBytes = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_remaining_at_large_bytes",
		Help:      "Bytes remaining in the at large byte allocation",
	})
	m.remainingVdrBytes = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_remaining_validator_bytes",
		Help:      "Bytes remaining in the validator byte allocation",
	})
	m.awaitingAcquire = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_awaiting_acquire",
		Help:      "Number of incoming messages waiting to be read",
	})
	m.awaitingRelease = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_awaiting_release",
		Help:      "Number of messages currently being read/handled",
	})
	errs := wrappers.Errs{}
	errs.Add(
		metricsRegisterer.Register(m.acquireLatency),
		metricsRegisterer.Register(m.remainingAtLargeBytes),
		metricsRegisterer.Register(m.remainingVdrBytes),
		metricsRegisterer.Register(m.awaitingAcquire),
		metricsRegisterer.Register(m.awaitingRelease),
	)
	return errs.Err
}

func NewNoThrottler() MsgThrottler {
	return &noMsgThrottler{}
}

// noMsgThrottler implements MsgThrottler.
// [Acquire] always returns immediately.
type noMsgThrottler struct{}

func (*noMsgThrottler) Acquire(uint64, ids.ShortID) {}

func (*noMsgThrottler) Release(uint64, ids.ShortID) {}
<commit_msg>clean up *sybilMsgThrottler Release<commit_after>// (c) 2021, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package throttling

import (
	"sync"
	"time"

	"github.com/ava-labs/avalanchego/ids"
	"github.com/ava-labs/avalanchego/snow/validators"
	"github.com/ava-labs/avalanchego/utils/constants"
	"github.com/ava-labs/avalanchego/utils/logging"
	"github.com/ava-labs/avalanchego/utils/math"
	"github.com/ava-labs/avalanchego/utils/metric"
	"github.com/ava-labs/avalanchego/utils/wrappers"
	"github.com/prometheus/client_golang/prometheus"
)

var (
	_ MsgThrottler = &noMsgThrottler{}
	_ MsgThrottler = &sybilMsgThrottler{}
)

// MsgThrottler rate-limits incoming messages from the network.
type MsgThrottler interface {
	// Blocks until node [nodeID] can put a message of
	// size [msgSize] onto the incoming message buffer.
	Acquire(msgSize uint64, nodeID ids.ShortID)

	// Mark that a message from [nodeID] of size [msgSize]
	// has been removed from the incoming message buffer.
	Release(msgSize uint64, nodeID ids.ShortID)
}

// See sybilMsgThrottler
type MsgThrottlerConfig struct {
	VdrAllocSize        uint64
	AtLargeAllocSize    uint64
	NodeMaxAtLargeBytes uint64
}

// Returns a new MsgThrottler.
// If this function returns an error, the returned MsgThrottler may still be used.
// However, some of its metrics may not be registered.
func NewSybilMsgThrottler(
	log logging.Logger,
	metricsRegisterer prometheus.Registerer,
	vdrs validators.Set,
	config MsgThrottlerConfig,
) (MsgThrottler, error) {
	t := &sybilMsgThrottler{
		log:                    log,
		cond:                   sync.NewCond(&sync.Mutex{}),
		vdrs:                   vdrs,
		maxVdrBytes:            config.VdrAllocSize,
		remainingVdrBytes:      config.VdrAllocSize,
		remainingAtLargeBytes:  config.AtLargeAllocSize,
		nodeMaxAtLargeBytes:    config.NodeMaxAtLargeBytes,
		nodeToVdrBytesUsed:     make(map[ids.ShortID]uint64),
		nodeToAtLargeBytesUsed: make(map[ids.ShortID]uint64),
	}
	if err := t.metrics.initialize(metricsRegisterer); err != nil {
		return nil, err
	}
	return t, nil
}

// msgThrottler implements MsgThrottler.
// It gives more space to validators with more stake.
type sybilMsgThrottler struct {
	log     logging.Logger
	metrics sybilMsgThrottlerMetrics
	cond    *sync.Cond
	// Primary network validator set
	vdrs validators.Set
	// Max number of unprocessed bytes from validators
	maxVdrBytes uint64
	// Max number of bytes that can be taken from the
	// at-large byte allocation by a given node.
	nodeMaxAtLargeBytes uint64
	// Number of bytes left in the validator byte allocation.
	// Initialized to [maxVdrBytes].
	remainingVdrBytes uint64
	// Number of bytes left in the at-large byte allocation
	remainingAtLargeBytes uint64
	// Node ID --> Bytes they've taken from the validator allocation
	nodeToVdrBytesUsed map[ids.ShortID]uint64
	// Node ID --> Bytes they've taken from the at-large allocation
	nodeToAtLargeBytesUsed map[ids.ShortID]uint64
}

// Returns when we can read a message of size [msgSize] from node [nodeID].
// Release([msgSize], [nodeID]) must be called (!) when done with the message
// or when we give up trying to read the message, if applicable.
func (t *sybilMsgThrottler) Acquire(msgSize uint64, nodeID ids.ShortID) {
	t.cond.L.Lock()
	defer t.cond.L.Unlock()

	t.metrics.awaitingAcquire.Inc()
	startTime := time.Now()

	for { // [t.cond.L] is held while in this loop
		atLargeBytesUsed := t.nodeToAtLargeBytesUsed[nodeID]
		// See if we can take from the at-large byte allocation
		if msgSize <= t.remainingAtLargeBytes && atLargeBytesUsed+msgSize <= t.nodeMaxAtLargeBytes {
			// Take from the at-large byte allocation
			t.remainingAtLargeBytes -= msgSize
			t.nodeToAtLargeBytesUsed[nodeID] += msgSize
			break
		}

		// See if we can use the validator byte allocation
		weight, isVdr := t.vdrs.GetWeight(nodeID)
		if !isVdr {
			// This node isn't a validator.
			// Wait until there are more bytes in an allocation.
			t.cond.Wait()
			continue
		}

		// From the at-large allocation, take all the bytes we can
		// without exceeding the per-node limit on taking from it
		atLargeBytesToUse := t.nodeMaxAtLargeBytes - atLargeBytesUsed
		if atLargeBytesToUse > t.remainingAtLargeBytes {
			atLargeBytesToUse = t.remainingAtLargeBytes
		}
		// Need [vdrBytesNeeded] from the validator allocation.
		vdrBytesNeeded := msgSize - atLargeBytesToUse
		if t.remainingVdrBytes < vdrBytesNeeded {
			// Wait until there are more bytes in an allocation.
			t.cond.Wait()
			continue
		}

		// Number of bytes this node can take from validator allocation.
		vdrBytesAllowed := uint64(0)
		// [totalVdrWeight] should always be > 0 but handle this case
		// for completeness to prevent divide by 0
		totalVdrWeight := t.vdrs.Weight()
		if totalVdrWeight != 0 {
			vdrBytesAllowed = uint64(float64(t.maxVdrBytes) * float64(weight) / float64(totalVdrWeight))
		} else {
			t.log.Warn("total validator weight is 0") // this should never happen
		}
		if t.nodeToVdrBytesUsed[nodeID]+vdrBytesNeeded > vdrBytesAllowed {
			// Wait until there are more bytes in an allocation.
			t.cond.Wait()
			continue
		}

		// Use some of [remainingAtLargeBytes] and some of [remainingVdrBytes]
		t.remainingVdrBytes -= vdrBytesNeeded
		if atLargeBytesToUse != 0 {
			t.nodeToAtLargeBytesUsed[nodeID] += atLargeBytesToUse
		}
		t.remainingAtLargeBytes -= atLargeBytesToUse
		t.nodeToVdrBytesUsed[nodeID] += vdrBytesNeeded
		break
	}
	t.metrics.acquireLatency.Observe(float64(time.Since(startTime)))
	t.metrics.remainingAtLargeBytes.Set(float64(t.remainingAtLargeBytes))
	t.metrics.remainingVdrBytes.Set(float64(t.remainingVdrBytes))
	t.metrics.awaitingAcquire.Dec()
	t.metrics.awaitingRelease.Inc()
}

// Must correspond to a previous call of Acquire([msgSize], [nodeID])
func (t *sybilMsgThrottler) Release(msgSize uint64, nodeID ids.ShortID) {
	t.cond.L.Lock()
	defer t.cond.L.Unlock()

	// Release as many bytes as possible back to [nodeID]'s validator allocation
	vdrBytesUsed := t.nodeToVdrBytesUsed[nodeID]
	vdrBytesReturned := math.Min64(msgSize, vdrBytesUsed)
	t.remainingVdrBytes += vdrBytesReturned
	t.nodeToVdrBytesUsed[nodeID] -= vdrBytesReturned
	if t.nodeToVdrBytesUsed[nodeID] == 0 {
		delete(t.nodeToVdrBytesUsed, nodeID)
	}

	// Release the rest of the bytes, if any, back to the at-large allocation
	atLargeBytesReturned := msgSize - vdrBytesReturned
	t.remainingAtLargeBytes += atLargeBytesReturned
	t.nodeToAtLargeBytesUsed[nodeID] -= atLargeBytesReturned
	if t.nodeToAtLargeBytesUsed[nodeID] == 0 {
		delete(t.nodeToAtLargeBytesUsed, nodeID)
	}

	t.metrics.remainingAtLargeBytes.Set(float64(t.remainingAtLargeBytes))
	t.metrics.remainingVdrBytes.Set(float64(t.remainingVdrBytes))
	t.metrics.awaitingRelease.Dec()

	// Notify that there are more bytes available
	t.cond.Broadcast()
}

type sybilMsgThrottlerMetrics struct {
	acquireLatency        prometheus.Histogram
	remainingAtLargeBytes prometheus.Gauge
	remainingVdrBytes     prometheus.Gauge
	awaitingAcquire       prometheus.Gauge
	awaitingRelease       prometheus.Gauge
}

func (m *sybilMsgThrottlerMetrics) initialize(metricsRegisterer prometheus.Registerer) error {
	m.acquireLatency = prometheus.NewHistogram(prometheus.HistogramOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_acquire_latency",
		Help:      "Duration an incoming message waited to be read due to throttling",
		Buckets:   metric.NanosecondsBuckets,
	})
	m.remainingAtLargeBytes = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_remaining_at_large_bytes",
		Help:      "Bytes remaining in the at large byte allocation",
	})
	m.remainingVdrBytes = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_remaining_validator_bytes",
		Help:      "Bytes remaining in the validator byte allocation",
	})
	m.awaitingAcquire = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_awaiting_acquire",
		Help:      "Number of incoming messages waiting to be read",
	})
	m.awaitingRelease = prometheus.NewGauge(prometheus.GaugeOpts{
		Namespace: constants.PlatformName,
		Name:      "throttler_awaiting_release",
		Help:      "Number of messages currently being read/handled",
	})
	errs := wrappers.Errs{}
	errs.Add(
		metricsRegisterer.Register(m.acquireLatency),
		metricsRegisterer.Register(m.remainingAtLargeBytes),
		metricsRegisterer.Register(m.remainingVdrBytes),
		metricsRegisterer.Register(m.awaitingAcquire),
		metricsRegisterer.Register(m.awaitingRelease),
	)
	return errs.Err
}

func NewNoThrottler() MsgThrottler {
	return &noMsgThrottler{}
}

// noMsgThrottler implements MsgThrottler.
// [Acquire] always returns immediately.
type noMsgThrottler struct{}

func (*noMsgThrottler) Acquire(uint64, ids.ShortID) {}

func (*noMsgThrottler) Release(uint64, ids.ShortID) {}
<|endoftext|>


<commit_before>package commands

import (
	"errors"
	"fmt"
	"github.com/github/git-media/gitmedia"
	"io/ioutil"
	"os"
	"path/filepath"
)

type LogsCommand struct {
	ClearLogs bool
	Boomtown  bool
	*Command
}

func (c *LogsCommand) Setup() {
	c.FlagSet.BoolVar(&c.ClearLogs, "clear", false, "Clear existing error logs")
	c.FlagSet.BoolVar(&c.Boomtown, "boomtown", false, "Trigger a panic")
}

func (c *LogsCommand) Run() {
	if c.ClearLogs {
		c.clear()
	}

	if c.Boomtown {
		c.boomtown()
		return
	}

	var sub string
	if len(c.SubCommands) > 0 {
		sub = c.SubCommands[0]
	}

	switch sub {
	case "last":
		c.lastLog()
	case "":
		c.listLogs()
	default:
		c.showLog(sub)
	}
}

func (c *LogsCommand) listLogs() {
	for _, path := range sortedLogs() {
		Print(path)
	}
}

func (c *LogsCommand) lastLog() {
	logs := sortedLogs()
	if len(logs) < 1 {
		Print("No logs to show")
		return
	}
	c.showLog(logs[len(logs)-1])
}

func (c *LogsCommand) showLog(name string) {
	by, err := ioutil.ReadFile(filepath.Join(gitmedia.LocalLogDir, name))
	if err != nil {
		Exit("Error reading log: %s", name)
	}

	Debug("Reading log: %s", name)
	os.Stdout.Write(by)
}

func (c *LogsCommand) clear() {
	err := os.RemoveAll(gitmedia.LocalLogDir)
	if err != nil {
		Panic(err, "Error clearing %s", gitmedia.LocalLogDir)
	}

	fmt.Println("Cleared", gitmedia.LocalLogDir)
}

func (c *LogsCommand) boomtown() {
	Debug("Debug message")
	err := errors.New("Error!")
	Panic(err, "Welcome to Boomtown")
	Debug("Never seen")
}

func sortedLogs() []string {
	fileinfos, err := ioutil.ReadDir(gitmedia.LocalLogDir)
	if err != nil {
		return []string{}
	}

	names := make([]string, len(fileinfos))
	for index, info := range fileinfos {
		names[index] = info.Name()
	}

	return names
}

func init() {
	registerCommand("logs", func(c *Command) RunnableCommand {
		return &LogsCommand{Command: c}
	})
}
<commit_msg>ンンン ンンン ンン<commit_after>package commands

import (
	"errors"
	"fmt"
	"github.com/github/git-media/gitmedia"
	"github.com/spf13/cobra"
	"io/ioutil"
	"os"
	"path/filepath"
)

var (
	clearLogsFlag bool
	boomtownFlag  bool

	logsCmd = &cobra.Command{
		Use:   "logs",
		Short: "View error logs.",
		Run:   logsCommand,
	}
)

func logsCommand(cmd *cobra.Command, args []string) {
	if clearLogsFlag {
		clearLogs()
	}

	if boomtownFlag {
		boomtown()
		return
	}

	var sub string
	if len(args) > 0 {
		sub = args[0]
	}

	switch sub {
	case "last":
		lastLog()
	case "":
		listLogs()
	default:
		showLog(sub)
	}
}

func listLogs() {
	for _, path := range sortedLogs() {
		Print(path)
	}
}

func lastLog() {
	logs := sortedLogs()
	if len(logs) < 1 {
		Print("No logs to show")
		return
	}
	showLog(logs[len(logs)-1])
}

func showLog(name string) {
	by, err := ioutil.ReadFile(filepath.Join(gitmedia.LocalLogDir, name))
	if err != nil {
		Exit("Error reading log: %s", name)
	}

	Debug("Reading log: %s", name)
	os.Stdout.Write(by)
}

func clearLogs() {
	err := os.RemoveAll(gitmedia.LocalLogDir)
	if err != nil {
		Panic(err, "Error clearing %s", gitmedia.LocalLogDir)
	}

	fmt.Println("Cleared", gitmedia.LocalLogDir)
}

func boomtown() {
	Debug("Debug message")
	err := errors.New("Error!")
	Panic(err, "Welcome to Boomtown")
	Debug("Never seen")
}

func sortedLogs() []string {
	fileinfos, err := ioutil.ReadDir(gitmedia.LocalLogDir)
	if err != nil {
		return []string{}
	}

	names := make([]string, len(fileinfos))
	for index, info := range fileinfos {
		names[index] = info.Name()
	}

	return names
}

func init() {
	logsCmd.Flags().BoolVarP(&clearLogsFlag, "clear", "c", false, "Clear existing error logs")
	logsCmd.Flags().BoolVarP(&boomtownFlag, "boomtown", "b", false, "Trigger a panic")
	RootCmd.AddCommand(logsCmd)
}
<|endoftext|>


<commit_before>package commands

import "testing"

func TestOptionValidation(t *testing.T) {
	cmd := Command{
		Options: []Option{
			Option{[]string{"b", "beep"}, Int, "enables beeper"},
			Option{[]string{"B", "boop"}, String, "password for booper"},
		},
		Run: func(res Response, req Request) {},
	}

	req := NewEmptyRequest()
	req.SetOption("beep", 5)
	req.SetOption("b", 10)
	res := cmd.Call(req)
	if res.Error() == nil {
		t.Error("Should have failed (duplicate options)")
	}

	req = NewEmptyRequest()
	req.SetOption("beep", "foo")
	res = cmd.Call(req)
	if res.Error() == nil {
		t.Error("Should have failed (incorrect type)")
	}

	req = NewEmptyRequest()
	req.SetOption("beep", 5)
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error(res.Error(), "Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("beep", 5)
	req.SetOption("boop", "test")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("b", 5)
	req.SetOption("B", "test")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("foo", 5)
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption(EncShort, "json")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("b", "100")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("b", ":)")
	res = cmd.Call(req)
	if res.Error() == nil {
		t.Error(res.Error(), "Should have failed (string value not convertible to int)")
	}
}

func TestRegistration(t *testing.T) {
	noop := func(res Response, req Request) {}

	cmdA := &Command{
		Options: []Option{
			Option{[]string{"beep"}, Int, "number of beeps"},
		},
		Run: noop,
	}

	cmdB := &Command{
		Options: []Option{
			Option{[]string{"beep"}, Int, "number of beeps"},
		},
		Run: noop,
		Subcommands: map[string]*Command{
			"a": cmdA,
		},
	}

	cmdC := &Command{
		Options: []Option{
			Option{[]string{"encoding"}, String, "data encoding type"},
		},
		Run: noop,
	}

	res := cmdB.Call(NewRequest([]string{"a"}, nil, nil, nil))
	if res.Error() == nil {
		t.Error("Should have failed (option name collision)")
	}

	res = cmdC.Call(NewEmptyRequest())
	if res.Error() == nil {
		t.Error("Should have failed (option name collision with global options)")
	}
}

func TestResolving(t *testing.T) {
	cmdC := &Command{}
	cmdB := &Command{
		Subcommands: map[string]*Command{
			"c": cmdC,
		},
	}
	cmdB2 := &Command{}
	cmdA := &Command{
		Subcommands: map[string]*Command{
			"b": cmdB,
			"B": cmdB2,
		},
	}
	cmd := &Command{
		Subcommands: map[string]*Command{
			"a": cmdA,
		},
	}

	cmds, err := cmd.Resolve([]string{"a", "b", "c"})
	if err != nil {
		t.Error(err)
	}
	if len(cmds) != 4 || cmds[0] != cmd || cmds[1] != cmdA || cmds[2] != cmdB || cmds[3] != cmdC {
		t.Error("Returned command path is different than expected", cmds)
	}
}
<commit_msg>commands: Fixed tests<commit_after>package commands

import "testing"

func noop(req Request) (interface{}, error) {
	return nil, nil
}

func TestOptionValidation(t *testing.T) {
	cmd := Command{
		Options: []Option{
			Option{[]string{"b", "beep"}, Int, "enables beeper"},
			Option{[]string{"B", "boop"}, String, "password for booper"},
		},
		Run: noop,
	}

	req := NewEmptyRequest()
	req.SetOption("beep", 5)
	req.SetOption("b", 10)
	res := cmd.Call(req)
	if res.Error() == nil {
		t.Error("Should have failed (duplicate options)")
	}

	req = NewEmptyRequest()
	req.SetOption("beep", "foo")
	res = cmd.Call(req)
	if res.Error() == nil {
		t.Error("Should have failed (incorrect type)")
	}

	req = NewEmptyRequest()
	req.SetOption("beep", 5)
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error(res.Error(), "Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("beep", 5)
	req.SetOption("boop", "test")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("b", 5)
	req.SetOption("B", "test")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("foo", 5)
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption(EncShort, "json")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("b", "100")
	res = cmd.Call(req)
	if res.Error() != nil {
		t.Error("Should have passed")
	}

	req = NewEmptyRequest()
	req.SetOption("b", ":)")
	res = cmd.Call(req)
	if res.Error() == nil {
		t.Error(res.Error(), "Should have failed (string value not convertible to int)")
	}
}

func TestRegistration(t *testing.T) {
	cmdA := &Command{
		Options: []Option{
			Option{[]string{"beep"}, Int, "number of beeps"},
		},
		Run: noop,
	}

	cmdB := &Command{
		Options: []Option{
			Option{[]string{"beep"}, Int, "number of beeps"},
		},
		Run: noop,
		Subcommands: map[string]*Command{
			"a": cmdA,
		},
	}

	cmdC := &Command{
		Options: []Option{
			Option{[]string{"encoding"}, String, "data encoding type"},
		},
		Run: noop,
	}

	res := cmdB.Call(NewRequest([]string{"a"}, nil, nil, nil))
	if res.Error() == nil {
		t.Error("Should have failed (option name collision)")
	}

	res = cmdC.Call(NewEmptyRequest())
	if res.Error() == nil {
		t.Error("Should have failed (option name collision with global options)")
	}
}

func TestResolving(t *testing.T) {
	cmdC := &Command{}
	cmdB := &Command{
		Subcommands: map[string]*Command{
			"c": cmdC,
		},
	}
	cmdB2 := &Command{}
	cmdA := &Command{
		Subcommands: map[string]*Command{
			"b": cmdB,
			"B": cmdB2,
		},
	}
	cmd := &Command{
		Subcommands: map[string]*Command{
			"a": cmdA,
		},
	}

	cmds, err := cmd.Resolve([]string{"a", "b", "c"})
	if err != nil {
		t.Error(err)
	}
	if len(cmds) != 4 || cmds[0] != cmd || cmds[1] != cmdA || cmds[2] != cmdB || cmds[3] != cmdC {
		t.Error("Returned command path is different than expected", cmds)
	}
}
<|endoftext|>


<commit_before>// AUTOGENERATED FILE; see hack/make/.go-autogen
package winresources

/*

This package is for embedding a manifest file and an icon into docker.exe.
The benefit of this is that a manifest file does not need to be alongside
the .exe, and there is an icon when docker runs, or viewed through Windows
explorer.

When make binary is run, the Dockerfile prepares the build environment by:

 - Cloning github.com/akavel/rsrc

 - Go-installing the rsrc executable

make.sh invokes hack/make/.go-autogen to:

 - Run rsrc to create a binary file (autogen/winresources/rsrc.syso) that
   contains the manifest and icon. This file is automatically picked up by
   'go build', so no post-processing steps are required. The sources for
   rsrc.syso are under hack/make/.resources-windows.

*/
<commit_msg>Remove autogen thing<commit_after><|endoftext|>


<commit_before>package shell

import (
	"flag"
	"fmt"
	"github.com/chrislusf/seaweedfs/weed/glog"
	"github.com/chrislusf/seaweedfs/weed/pb"
	"github.com/chrislusf/seaweedfs/weed/pb/master_pb"
	"github.com/chrislusf/seaweedfs/weed/storage/types"
	"github.com/chrislusf/seaweedfs/weed/wdclient"
	"io"
	"path/filepath"
	"sync"
	"time"

	"github.com/chrislusf/seaweedfs/weed/storage/needle"
)

func init() {
	Commands = append(Commands, &commandVolumeTierMove{})
}

type commandVolumeTierMove struct {
	activeServers     map[pb.ServerAddress]struct{}
	activeServersLock sync.Mutex
	activeServersCond *sync.Cond
}

func (c *commandVolumeTierMove) Name() string {
	return "volume.tier.move"
}

func (c *commandVolumeTierMove) Help() string {
	return `change a volume from one disk type to another

	volume.tier.move -fromDiskType=hdd -toDiskType=ssd [-collectionPattern=""] [-fullPercent=95] [-quietFor=1h]

	Even if the volume is replicated, only one replica will be changed and the rest replicas will be dropped.
	So "volume.fix.replication" and "volume.balance" should be followed.

`
}

func (c *commandVolumeTierMove) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {

	c.activeServers = make(map[pb.ServerAddress]struct{})
	c.activeServersCond = sync.NewCond(new(sync.Mutex))

	tierCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
	collectionPattern := tierCommand.String("collectionPattern", "", "match with wildcard characters '*' and '?'")
	fullPercentage := tierCommand.Float64("fullPercent", 95, "the volume reaches the percentage of max volume size")
	quietPeriod := tierCommand.Duration("quietFor", 24*time.Hour, "select volumes without no writes for this period")
	source := tierCommand.String("fromDiskType", "", "the source disk type")
	target := tierCommand.String("toDiskType", "", "the target disk type")
	applyChange := tierCommand.Bool("force", false, "actually apply the changes")
	if err = tierCommand.Parse(args); err != nil {
		return nil
	}

	if err = commandEnv.confirmIsLocked(args); err != nil {
		return
	}

	fromDiskType := types.ToDiskType(*source)
	toDiskType := types.ToDiskType(*target)

	if fromDiskType == toDiskType {
		return fmt.Errorf("source tier %s is the same as target tier %s", fromDiskType, toDiskType)
	}

	// collect topology information
	topologyInfo, volumeSizeLimitMb, err := collectTopologyInfo(commandEnv)
	if err != nil {
		return err
	}

	// collect all volumes that should change
	volumeIds, err := collectVolumeIdsForTierChange(commandEnv, topologyInfo, volumeSizeLimitMb, fromDiskType, *collectionPattern, *fullPercentage, *quietPeriod)
	if err != nil {
		return err
	}
	fmt.Printf("tier move volumes: %v\n", volumeIds)

	_, allLocations := collectVolumeReplicaLocations(topologyInfo)
	for _, vid := range volumeIds {
		if err = c.doVolumeTierMove(commandEnv, writer, vid, toDiskType, allLocations, *applyChange); err != nil {
			fmt.Printf("tier move volume %d: %v\n", vid, err)
		}
	}

	return nil
}

func isOneOf(server string, locations []wdclient.Location) bool {
	for _, loc := range locations {
		if server == loc.Url {
			return true
		}
	}
	return false
}

func (c *commandVolumeTierMove) doVolumeTierMove(commandEnv *CommandEnv, writer io.Writer, vid needle.VolumeId, toDiskType types.DiskType, allLocations []location, applyChanges bool) (err error) {
	// find volume location
	locations, found := commandEnv.MasterClient.GetLocations(uint32(vid))
	if !found {
		return fmt.Errorf("volume %d not found", vid)
	}

	// find one server with the most empty volume slots with target disk type
	hasFoundTarget := false
	keepDataNodesSorted(allLocations, toDiskType)
	fn := capacityByFreeVolumeCount(toDiskType)
	wg := sync.WaitGroup{}
	for _, dst := range allLocations {
		if fn(dst.dataNode) > 0 && !hasFoundTarget {
			// ask the volume server to replicate the volume
			if isOneOf(dst.dataNode.Id, locations) {
				continue
			}
			var sourceVolumeServer pb.ServerAddress
			for _, loc := range locations {
				if loc.Url != dst.dataNode.Id {
					sourceVolumeServer = loc.ServerAddress()
				}
			}
			if sourceVolumeServer == "" {
				continue
			}
			fmt.Fprintf(writer, "moving volume %d from %s to %s with disk type %s ...\n", vid, sourceVolumeServer, dst.dataNode.Id, toDiskType.ReadableString())
			hasFoundTarget = true

			if !applyChanges {
				// adjust volume count
				dst.dataNode.DiskInfos[string(toDiskType)].VolumeCount++
				break
			}

			destServerAddress := pb.NewServerAddressFromDataNode(dst.dataNode)
			c.activeServersCond.L.Lock()
			_, isSourceActive := c.activeServers[sourceVolumeServer]
			_, isDestActive := c.activeServers[destServerAddress]
			for isSourceActive || isDestActive {
				c.activeServersCond.Wait()
				_, isSourceActive = c.activeServers[sourceVolumeServer]
				_, isDestActive = c.activeServers[destServerAddress]
			}
			c.activeServers[sourceVolumeServer] = struct{}{}
			c.activeServers[destServerAddress] = struct{}{}
			c.activeServersCond.L.Unlock()

			wg.Add(1)
			go func(dst location) {
				if err := c.doMoveOneVolume(commandEnv, writer, vid, toDiskType, locations, sourceVolumeServer, dst); err != nil {
					fmt.Fprintf(writer, "move volume %d %s => %s: %v\n", vid, sourceVolumeServer, dst.dataNode.Id, err)
				}
				delete(c.activeServers, sourceVolumeServer)
				delete(c.activeServers, destServerAddress)
				c.activeServersCond.Signal()
				wg.Done()
			}(dst)

		}
	}

	wg.Wait()

	if !hasFoundTarget {
		fmt.Fprintf(writer, "can not find disk type %s for volume %d\n", toDiskType.ReadableString(), vid)
	}

	return nil
}

func (c *commandVolumeTierMove) doMoveOneVolume(commandEnv *CommandEnv, writer io.Writer, vid needle.VolumeId, toDiskType types.DiskType, locations []wdclient.Location, sourceVolumeServer pb.ServerAddress, dst location) (err error) {

	// mark all replicas as read only
	if err = markVolumeReplicasWritable(commandEnv.option.GrpcDialOption, vid, locations, false); err != nil {
		return fmt.Errorf("mark volume %d as readonly on %s: %v", vid, locations[0].Url, err)
	}
	if err = LiveMoveVolume(commandEnv.option.GrpcDialOption, writer, vid, sourceVolumeServer, pb.NewServerAddressFromDataNode(dst.dataNode), 5*time.Second, toDiskType.ReadableString(), true); err != nil {

		// mark all replicas as writable
		if err = markVolumeReplicasWritable(commandEnv.option.GrpcDialOption, vid, locations, true); err != nil {
			glog.Errorf("mark volume %d as writable on %s: %v", vid, locations[0].Url, err)
		}

		return fmt.Errorf("move volume %d %s => %s : %v", vid, locations[0].Url, dst.dataNode.Id, err)
	}

	// adjust volume count
	dst.dataNode.DiskInfos[string(toDiskType)].VolumeCount++

	// remove the remaining replicas
	for _, loc := range locations {
		if loc.Url != dst.dataNode.Id && loc.ServerAddress() != sourceVolumeServer {
			if err = deleteVolume(commandEnv.option.GrpcDialOption, vid, loc.ServerAddress()); err != nil {
				fmt.Fprintf(writer, "failed to delete volume %d on %s: %v\n", vid, loc.Url, err)
			}
		}
	}
	return nil
}

func collectVolumeIdsForTierChange(commandEnv *CommandEnv, topologyInfo *master_pb.TopologyInfo, volumeSizeLimitMb uint64, sourceTier types.DiskType, collectionPattern string, fullPercentage float64, quietPeriod time.Duration) (vids []needle.VolumeId, err error) {

	quietSeconds := int64(quietPeriod / time.Second)
	nowUnixSeconds := time.Now().Unix()

	fmt.Printf("collect %s volumes quiet for: %d seconds\n", sourceTier, quietSeconds)

	vidMap := make(map[uint32]bool)
	eachDataNode(topologyInfo, func(dc string, rack RackId, dn *master_pb.DataNodeInfo) {
		for _, diskInfo := range dn.DiskInfos {
			for _, v := range diskInfo.VolumeInfos {
				// check collection name pattern
				if collectionPattern != "" {
					matched, err := filepath.Match(collectionPattern, v.Collection)
					if err != nil {
						return
					}
					if !matched {
						continue
					}
				}

				if v.ModifiedAtSecond+quietSeconds < nowUnixSeconds && types.ToDiskType(v.DiskType) == sourceTier {
					if float64(v.Size) > fullPercentage/100*float64(volumeSizeLimitMb)*1024*1024 {
						vidMap[v.Id] = true
					}
				}
			}
		}
	})

	for vid := range vidMap {
		vids = append(vids, needle.VolumeId(vid))
	}

	return
}
<commit_msg>async volumeTierMove<commit_after>package shell

import (
	"flag"
	"fmt"
	"github.com/chrislusf/seaweedfs/weed/glog"
	"github.com/chrislusf/seaweedfs/weed/pb"
	"github.com/chrislusf/seaweedfs/weed/pb/master_pb"
	"github.com/chrislusf/seaweedfs/weed/storage/types"
	"github.com/chrislusf/seaweedfs/weed/wdclient"
	"io"
	"path/filepath"
	"sync"
	"time"

	"github.com/chrislusf/seaweedfs/weed/storage/needle"
)

func init() {
	Commands = append(Commands, &commandVolumeTierMove{})
}

type volumeTierMoveJob struct {
	src pb.ServerAddress
	vid needle.VolumeId
}

type commandVolumeTierMove struct {
	activeServers sync.Map
	queues map[pb.ServerAddress]chan volumeTierMoveJob
	//activeServers     map[pb.ServerAddress]struct{}
	//activeServersLock sync.Mutex
	//activeServersCond *sync.Cond
}

func (c *commandVolumeTierMove) Name() string {
	return "volume.tier.move"
}

func (c *commandVolumeTierMove) Help() string {
	return `change a volume from one disk type to another

	volume.tier.move -fromDiskType=hdd -toDiskType=ssd [-collectionPattern=""] [-fullPercent=95] [-quietFor=1h]

	Even if the volume is replicated, only one replica will be changed and the rest replicas will be dropped.
	So "volume.fix.replication" and "volume.balance" should be followed.

`
}

func (c *commandVolumeTierMove) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {

	tierCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
	collectionPattern := tierCommand.String("collectionPattern", "", "match with wildcard characters '*' and '?'")
	fullPercentage := tierCommand.Float64("fullPercent", 95, "the volume reaches the percentage of max volume size")
	quietPeriod := tierCommand.Duration("quietFor", 24*time.Hour, "select volumes without no writes for this period")
	source := tierCommand.String("fromDiskType", "", "the source disk type")
	target := tierCommand.String("toDiskType", "", "the target disk type")
	limitWorkers := tierCommand.Int("limitWorkers", 0, "limit the number of active copying workers")
	applyChange := tierCommand.Bool("force", false, "actually apply the changes")
	if err = tierCommand.Parse(args); err != nil {
		return nil
	}

	if err = commandEnv.confirmIsLocked(args); err != nil {
		return
	}

	fromDiskType := types.ToDiskType(*source)
	toDiskType := types.ToDiskType(*target)

	if fromDiskType == toDiskType {
		return fmt.Errorf("source tier %s is the same as target tier %s", fromDiskType, toDiskType)
	}

	// collect topology information
	topologyInfo, volumeSizeLimitMb, err := collectTopologyInfo(commandEnv)
	if err != nil {
		return err
	}

	// collect all volumes that should change
	volumeIds, err := collectVolumeIdsForTierChange(commandEnv, topologyInfo, volumeSizeLimitMb, fromDiskType, *collectionPattern, *fullPercentage, *quietPeriod)
	if err != nil {
		return err
	}
	fmt.Printf("tier move volumes: %v\n", volumeIds)

	_, allLocations := collectVolumeReplicaLocations(topologyInfo)
	allLocations = filterLocationsByDiskType(allLocations, toDiskType)
	keepDataNodesSorted(allLocations, toDiskType)

	if len(allLocations) > 0 &&  *limitWorkers > 0 && *limitWorkers < len(allLocations) {
		allLocations = allLocations[:*limitWorkers]
	}

	wg := sync.WaitGroup{}
	bufferLen := len(allLocations)
	c.queues = make(map[pb.ServerAddress]chan volumeTierMoveJob)

	for _, dst := range allLocations {
		destServerAddress := pb.NewServerAddressFromDataNode(dst.dataNode)
		c.queues[destServerAddress] = make(chan volumeTierMoveJob, bufferLen)

		wg.Add(1)
		go func (dst location, jobs <-chan volumeTierMoveJob, applyChanges bool) {
			for job := range jobs {
				fmt.Fprintf(writer, "moving volume %d from %s to %s with disk type %s ...\n", job.vid, job.src, dst.dataNode.Id, toDiskType.ReadableString())
				if !applyChanges {
					continue
				}

				locations, found := commandEnv.MasterClient.GetLocations(uint32(job.vid))
				if !found {
					fmt.Printf("volume %d not found", job.vid)
					continue
				}

				unlock := c.Lock(job.src)

				if err := c.doMoveOneVolume(commandEnv, writer, job.vid, toDiskType, locations, job.src, dst); err != nil {
					fmt.Fprintf(writer, "move volume %d %s => %s: %v\n", job.vid, job.src, dst.dataNode.Id, err)
				}
				unlock()
			}
			wg.Done()
		}(dst, c.queues[destServerAddress], *applyChange)
	}

	for _, vid := range volumeIds {
		if err = c.doVolumeTierMove(commandEnv, writer, vid, toDiskType, allLocations); err != nil {
			fmt.Printf("tier move volume %d: %v\n", vid, err)
		}
		allLocations = rotateDataNodes(allLocations)
	}
	for key, _ := range c.queues {
		close(c.queues[key])
	}

	wg.Wait()

	return nil
}

func (c *commandVolumeTierMove) Lock(key pb.ServerAddress) func() {
	value, _ := c.activeServers.LoadOrStore(key, &sync.Mutex{})
	mtx := value.(*sync.Mutex)
	mtx.Lock()

	return func() { mtx.Unlock() }
}

func filterLocationsByDiskType(dataNodes []location, diskType types.DiskType) (ret []location) {
	for _, loc := range dataNodes {
		_, found := loc.dataNode.DiskInfos[string(diskType)]
		if found {
			ret = append(ret, loc)
		}
	}
	return
}

func rotateDataNodes(dataNodes []location) []location {
	if len(dataNodes) > 0 {
		return append(dataNodes[1:], dataNodes[0])
	} else {
		return dataNodes
	}
}

func isOneOf(server string, locations []wdclient.Location) bool {
	for _, loc := range locations {
		if server == loc.Url {
			return true
		}
	}
	return false
}

func (c *commandVolumeTierMove) doVolumeTierMove(commandEnv *CommandEnv, writer io.Writer, vid needle.VolumeId, toDiskType types.DiskType, allLocations []location) (err error) {
	// find volume location
	locations, found := commandEnv.MasterClient.GetLocations(uint32(vid))
	if !found {
		return fmt.Errorf("volume %d not found", vid)
	}

	// find one server with the most empty volume slots with target disk type
	hasFoundTarget := false
	fn := capacityByFreeVolumeCount(toDiskType)
	for _, dst := range allLocations {
		if fn(dst.dataNode) > 0 && !hasFoundTarget {
			// ask the volume server to replicate the volume
			if isOneOf(dst.dataNode.Id, locations) {
				continue
			}
			var sourceVolumeServer pb.ServerAddress
			for _, loc := range locations {
				if loc.Url != dst.dataNode.Id {
					sourceVolumeServer = loc.ServerAddress()
				}
			}
			if sourceVolumeServer == "" {
				continue
			}
			hasFoundTarget = true

			// adjust volume count
			dst.dataNode.DiskInfos[string(toDiskType)].VolumeCount++

			destServerAddress := pb.NewServerAddressFromDataNode(dst.dataNode)
			c.queues[destServerAddress] <- volumeTierMoveJob{sourceVolumeServer, vid}
		}
	}

	if !hasFoundTarget {
		fmt.Fprintf(writer, "can not find disk type %s for volume %d\n", toDiskType.ReadableString(), vid)
	}

	return nil
}

func (c *commandVolumeTierMove) doMoveOneVolume(commandEnv *CommandEnv, writer io.Writer, vid needle.VolumeId, toDiskType types.DiskType, locations []wdclient.Location, sourceVolumeServer pb.ServerAddress, dst location) (err error) {

	// mark all replicas as read only
	if err = markVolumeReplicasWritable(commandEnv.option.GrpcDialOption, vid, locations, false); err != nil {
		return fmt.Errorf("mark volume %d as readonly on %s: %v", vid, locations[0].Url, err)
	}
	if err = LiveMoveVolume(commandEnv.option.GrpcDialOption, writer, vid, sourceVolumeServer, pb.NewServerAddressFromDataNode(dst.dataNode), 5*time.Second, toDiskType.ReadableString(), true); err != nil {

		// mark all replicas as writable
		if err = markVolumeReplicasWritable(commandEnv.option.GrpcDialOption, vid, locations, true); err != nil {
			glog.Errorf("mark volume %d as writable on %s: %v", vid, locations[0].Url, err)
		}

		return fmt.Errorf("move volume %d %s => %s : %v", vid, locations[0].Url, dst.dataNode.Id, err)
	}

	// adjust volume count
	dst.dataNode.DiskInfos[string(toDiskType)].VolumeCount++

	// remove the remaining replicas
	for _, loc := range locations {
		if loc.Url != dst.dataNode.Id && loc.ServerAddress() != sourceVolumeServer {
			if err = deleteVolume(commandEnv.option.GrpcDialOption, vid, loc.ServerAddress()); err != nil {
				fmt.Fprintf(writer, "failed to delete volume %d on %s: %v\n", vid, loc.Url, err)
			}
		}
	}
	return nil
}

func collectVolumeIdsForTierChange(commandEnv *CommandEnv, topologyInfo *master_pb.TopologyInfo, volumeSizeLimitMb uint64, sourceTier types.DiskType, collectionPattern string, fullPercentage float64, quietPeriod time.Duration) (vids []needle.VolumeId, err error) {

	quietSeconds := int64(quietPeriod / time.Second)
	nowUnixSeconds := time.Now().Unix()

	fmt.Printf("collect %s volumes quiet for: %d seconds\n", sourceTier, quietSeconds)

	vidMap := make(map[uint32]bool)
	eachDataNode(topologyInfo, func(dc string, rack RackId, dn *master_pb.DataNodeInfo) {
		for _, diskInfo := range dn.DiskInfos {
			for _, v := range diskInfo.VolumeInfos {
				// check collection name pattern
				if collectionPattern != "" {
					matched, err := filepath.Match(collectionPattern, v.Collection)
					if err != nil {
						return
					}
					if !matched {
						continue
					}
				}

				if v.ModifiedAtSecond+quietSeconds < nowUnixSeconds && types.ToDiskType(v.DiskType) == sourceTier {
					if float64(v.Size) > fullPercentage/100*float64(volumeSizeLimitMb)*1024*1024 {
						vidMap[v.Id] = true
					}
				}
			}
		}
	})

	for vid := range vidMap {
		vids = append(vids, needle.VolumeId(vid))
	}

	return
}
<|endoftext|>


<commit_before>package gmqnet

import (
	"encoding/json"
)

type Message struct {
	Operation byte   `json:"operation"`
	Payload   []byte `json:"payload"`
	Confirmed byte   `json:"confirm"`
}

func ParseMessage(in []byte) (*Message, error) {
	m := new(Message)
	return m, json.Unmarshal(in, m)
}
<commit_msg>Added model<commit_after>package gmqnet

import (
	"encoding/json"
)

type Message struct {
	Operation byte   `json:"operation"`
	Queue     string `json:"queue"`
	Payload   []byte `json:"payload"`
	Confirmed byte   `json:"confirmation"`
}

func ParseMessage(in []byte) (*Message, error) {
	m := new(Message)
	return m, json.Unmarshal(in, m)
}

func WriteMessage(m *Message) ([]byte, error) {
	return json.Marshal(m)
}
<|endoftext|>


<commit_before>// Copyright ©2012 Dan Kortschak <dan.kortschak@adelaide.edu.au>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package kdtree

import (
	"fmt"
	"math/rand"
	"sort"
)

// Partition partitions list such that all elements less than the value at pivot prior to the
// call are placed before that element and all elements greater than that value are placed after it.
// The final location of the element at pivot prior to the call is returned.
func Partition(list sort.Interface, pivot int) int {
	var index, last int
	if last = list.Len() - 1; last < 0 {
		return -1
	}
	list.Swap(pivot, last)
	for i := 0; i < last; i++ {
		if !list.Less(last, i) {
			list.Swap(index, i)
			index++
		}
	}
	list.Swap(last, index)
	return index
}

// A SortSlicer satisfies the sort.Interface and is able to slice itself.
type SortSlicer interface {
	sort.Interface
	Slice(start, end int) SortSlicer
}

// Select partitions list such that all elements less than the kth largest element are
// placed placed before k in the resulting list and all elements greater than it are placed
// after the position k.
func Select(list SortSlicer, k int) int {
	var (
		start int
		e     = list.Len()
	)
	if k >= e {
		if k == 0 {
			return 0
		}
		panic(fmt.Sprintf("kdtree: index out of range"))
	}
	if start == e-1 {
		return k
	}

	for {
		if start == e {
			panic("kdtree: internal inconsistency")
		}
		sub := list.Slice(start, e)
		pivot := Partition(sub, rand.Intn(sub.Len()))
		switch {
		case pivot == k:
			return k
		case k < pivot:
			e = pivot + start
		default:
			k -= pivot
			start += pivot
		}
	}

	panic("cannot reach")
}

func min(a, b int) int {
	if a > b {
		return b
	}
	return a
}

// MedianOfMedians returns the index to the median value of the medians of groups of 5 consecutive elements.
func MedianOfMedians(list SortSlicer) int {
	n := list.Len() / 5
	for i := 0; i < n; i++ {
		left := i * 5
		sub := list.Slice(left, min(left+5, list.Len()-1))
		Select(sub, 2)
		list.Swap(i, left+2)
	}
	Select(list.Slice(0, min(n, list.Len()-1)), min(list.Len(), n/2))
	return n / 2
}

// RandomOfMedians returns the index to the median value of up to n randomly chosen elements in list.
func MedianOfRandoms(list SortSlicer, n int) int {
	if l := list.Len(); n <= l {
		for i := 0; i < n; i++ {
			list.Swap(i, rand.Intn(n))
		}
	} else {
		n = l
	}
	Select(list.Slice(0, n), n/2)
	return n / 2
}
<commit_msg>Finish name change edit: e -> end (to match start)<commit_after>// Copyright ©2012 Dan Kortschak <dan.kortschak@adelaide.edu.au>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package kdtree

import (
	"fmt"
	"math/rand"
	"sort"
)

// Partition partitions list such that all elements less than the value at pivot prior to the
// call are placed before that element and all elements greater than that value are placed after it.
// The final location of the element at pivot prior to the call is returned.
func Partition(list sort.Interface, pivot int) int {
	var index, last int
	if last = list.Len() - 1; last < 0 {
		return -1
	}
	list.Swap(pivot, last)
	for i := 0; i < last; i++ {
		if !list.Less(last, i) {
			list.Swap(index, i)
			index++
		}
	}
	list.Swap(last, index)
	return index
}

// A SortSlicer satisfies the sort.Interface and is able to slice itself.
type SortSlicer interface {
	sort.Interface
	Slice(start, end int) SortSlicer
}

// Select partitions list such that all elements less than the kth largest element are
// placed placed before k in the resulting list and all elements greater than it are placed
// after the position k.
func Select(list SortSlicer, k int) int {
	var (
		start int
		end   = list.Len()
	)
	if k >= end {
		if k == 0 {
			return 0
		}
		panic(fmt.Sprintf("kdtree: index out of range"))
	}
	if start == end-1 {
		return k
	}

	for {
		if start == end {
			panic("kdtree: internal inconsistency")
		}
		sub := list.Slice(start, end)
		pivot := Partition(sub, rand.Intn(sub.Len()))
		switch {
		case pivot == k:
			return k
		case k < pivot:
			end = pivot + start
		default:
			k -= pivot
			start += pivot
		}
	}

	panic("cannot reach")
}

func min(a, b int) int {
	if a > b {
		return b
	}
	return a
}

// MedianOfMedians returns the index to the median value of the medians of groups of 5 consecutive elements.
func MedianOfMedians(list SortSlicer) int {
	n := list.Len() / 5
	for i := 0; i < n; i++ {
		left := i * 5
		sub := list.Slice(left, min(left+5, list.Len()-1))
		Select(sub, 2)
		list.Swap(i, left+2)
	}
	Select(list.Slice(0, min(n, list.Len()-1)), min(list.Len(), n/2))
	return n / 2
}

// RandomOfMedians returns the index to the median value of up to n randomly chosen elements in list.
func MedianOfRandoms(list SortSlicer, n int) int {
	if l := list.Len(); n <= l {
		for i := 0; i < n; i++ {
			list.Swap(i, rand.Intn(n))
		}
	} else {
		n = l
	}
	Select(list.Slice(0, n), n/2)
	return n / 2
}
<|endoftext|>


<commit_before>package mit

import (
	"testing"
	"time"
)

var testTerm = TermInfo{
	Code:              "2021FA",
	FirstDayOfClasses: time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC),
	LastDayOfClasses:  time.Date(2020, 12, 9, 0, 0, 0, 0, time.UTC),
	ExceptionDays:     map[string]time.Weekday{},
}

func termDate(year int, month time.Month, day int) *time.Time {
	result := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
	return &result
}

func compareWeekdays(a []time.Weekday, b []time.Weekday) bool {
	if len(a) != len(b) {
		return false
	}

	for i := 0; i < len(a); i++ {
		if a[i] != b[i] {
			return false
		}
	}

	return true
}

func testSingleTime(t *testing.T, input string, expectedScheduledMeeting *ScheduledMeeting, expectedStart *time.Time, expectedEnd *time.Time) {
	resultScheduledMeeting, resultStart, resultEnd, err := ParseScheduledMeeting(input, testTerm)
	if err != nil {
		t.Errorf("ParseScheduledMeeting('%s'): got error '%s'", input, err.Error())
		return
	}

	if !compareWeekdays(resultScheduledMeeting.Weekdays, expectedScheduledMeeting.Weekdays) {
		t.Errorf("ParseScheduledMeeting('%s'): ScheduledMeeting.Weekdays: got %#v, expected %#v", input, resultScheduledMeeting.Weekdays, expectedScheduledMeeting.Weekdays)
	}

	if resultScheduledMeeting.StartSeconds != expectedScheduledMeeting.StartSeconds {
		t.Errorf("ParseScheduledMeeting('%s'): ScheduledMeeting.StartSeconds: got %d, expected %d", input, resultScheduledMeeting.StartSeconds, expectedScheduledMeeting.StartSeconds)
	}

	if resultScheduledMeeting.EndSeconds != expectedScheduledMeeting.EndSeconds {
		t.Errorf("ParseScheduledMeeting('%s'): ScheduledMeeting.EndSeconds: got %d, expected %d", input, resultScheduledMeeting.EndSeconds, expectedScheduledMeeting.EndSeconds)
	}

	if (resultStart == nil && resultStart != expectedStart) || (resultStart != nil && *resultStart != *expectedStart) {
		t.Errorf("ParseScheduledMeeting('%s'): start date: got %s, expected %s", input, resultStart, expectedStart)
	}

	if (resultEnd == nil && resultEnd != expectedEnd) || (resultEnd != nil && *resultEnd != *expectedEnd) {
		t.Errorf("ParseScheduledMeeting('%s'): end date: got %s, expected %s", input, resultEnd, expectedEnd)
	}
}

const minuteSeconds = 60
const hourSeconds = 60 * minuteSeconds

func TestTimeParser(t *testing.T) {
	/*
	 * well-formed
	 */

	testSingleTime(t, "T3", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday},
		StartSeconds: 15 * hourSeconds,
		EndSeconds:   16 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "W3.30", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday},
		StartSeconds: (15 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (16 * hourSeconds) + (30 * minuteSeconds),
	}, nil, nil)
	testSingleTime(t, "R1.30-3.30", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Thursday},
		StartSeconds: (13 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (15 * hourSeconds) + (30 * minuteSeconds),
	}, nil, nil)
	testSingleTime(t, "S4", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Saturday},
		StartSeconds: 16 * hourSeconds,
		EndSeconds:   17 * hourSeconds,
	}, nil, nil)

	testSingleTime(t, "MTF11", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Tuesday, time.Friday},
		StartSeconds: 11 * hourSeconds,
		EndSeconds:   12 * hourSeconds,
	}, nil, nil)

	testSingleTime(t, "TR10.30-12 (BEGINS OCT 21)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: (10 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   12 * hourSeconds,
	}, termDate(2020, time.October, 21), nil)

	testSingleTime(t, "RF11.30 (ENDS DEC 2)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Thursday, time.Friday},
		StartSeconds: (11 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (12 * hourSeconds) + (30 * minuteSeconds),
	}, nil, termDate(2020, time.December, 2))

	testSingleTime(t, "MT9 (MEETS 9/4 TO 10/6)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Tuesday},
		StartSeconds: 9 * hourSeconds,
		EndSeconds:   10 * hourSeconds,
	}, termDate(2020, time.September, 4), termDate(2020, time.October, 6))

	testSingleTime(t, "WF EVE (5-7)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday, time.Friday},
		StartSeconds: 17 * hourSeconds,
		EndSeconds:   19 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "TR EVE (4-6 PM)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: 16 * hourSeconds,
		EndSeconds:   18 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "MW EVE (4.30-5.30 PM) (BEGINS NOV 2)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Wednesday},
		StartSeconds: (16 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (17 * hourSeconds) + (30 * minuteSeconds),
	}, termDate(2020, time.November, 2), nil)

	testSingleTime(t, "MW10 (LIMITED)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Wednesday},
		StartSeconds: 10 * hourSeconds,
		EndSeconds:   11 * hourSeconds,
	}, nil, nil)

	testSingleTime(t, "WR3.30-4.45 (LIMITED) (BEGINS OCT 14) (ENDS OCT 23)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday, time.Thursday},
		StartSeconds: (15 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (16 * hourSeconds) + (45 * minuteSeconds),
	}, termDate(2020, time.October, 14), termDate(2020, time.October, 23))

	testSingleTime(t, "WR EVE (3.30-4.45 PM) (LIMITED) (BEGINS OCT 14) (ENDS OCT 23)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday, time.Thursday},
		StartSeconds: (15 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (16 * hourSeconds) + (45 * minuteSeconds),
	}, termDate(2020, time.October, 14), termDate(2020, time.October, 23))

	/*
	 * not well-formed
	 */

	testSingleTime(t, "W 4:30", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday},
		StartSeconds: (16 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (17 * hourSeconds) + (30 * minuteSeconds),
	}, nil, nil)
	testSingleTime(t, "TR 10:30-12p", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: (10 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   12 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "TTH 10:30 - 12 PM", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: (10 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   12 * hourSeconds,
	}, nil, nil)
}
<commit_msg>mit/parse_test: add 'Th 4:30' test case<commit_after>package mit

import (
	"testing"
	"time"
)

var testTerm = TermInfo{
	Code:              "2021FA",
	FirstDayOfClasses: time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC),
	LastDayOfClasses:  time.Date(2020, 12, 9, 0, 0, 0, 0, time.UTC),
	ExceptionDays:     map[string]time.Weekday{},
}

func termDate(year int, month time.Month, day int) *time.Time {
	result := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
	return &result
}

func compareWeekdays(a []time.Weekday, b []time.Weekday) bool {
	if len(a) != len(b) {
		return false
	}

	for i := 0; i < len(a); i++ {
		if a[i] != b[i] {
			return false
		}
	}

	return true
}

func testSingleTime(t *testing.T, input string, expectedScheduledMeeting *ScheduledMeeting, expectedStart *time.Time, expectedEnd *time.Time) {
	resultScheduledMeeting, resultStart, resultEnd, err := ParseScheduledMeeting(input, testTerm)
	if err != nil {
		t.Errorf("ParseScheduledMeeting('%s'): got error '%s'", input, err.Error())
		return
	}

	if !compareWeekdays(resultScheduledMeeting.Weekdays, expectedScheduledMeeting.Weekdays) {
		t.Errorf("ParseScheduledMeeting('%s'): ScheduledMeeting.Weekdays: got %#v, expected %#v", input, resultScheduledMeeting.Weekdays, expectedScheduledMeeting.Weekdays)
	}

	if resultScheduledMeeting.StartSeconds != expectedScheduledMeeting.StartSeconds {
		t.Errorf("ParseScheduledMeeting('%s'): ScheduledMeeting.StartSeconds: got %d, expected %d", input, resultScheduledMeeting.StartSeconds, expectedScheduledMeeting.StartSeconds)
	}

	if resultScheduledMeeting.EndSeconds != expectedScheduledMeeting.EndSeconds {
		t.Errorf("ParseScheduledMeeting('%s'): ScheduledMeeting.EndSeconds: got %d, expected %d", input, resultScheduledMeeting.EndSeconds, expectedScheduledMeeting.EndSeconds)
	}

	if (resultStart == nil && resultStart != expectedStart) || (resultStart != nil && *resultStart != *expectedStart) {
		t.Errorf("ParseScheduledMeeting('%s'): start date: got %s, expected %s", input, resultStart, expectedStart)
	}

	if (resultEnd == nil && resultEnd != expectedEnd) || (resultEnd != nil && *resultEnd != *expectedEnd) {
		t.Errorf("ParseScheduledMeeting('%s'): end date: got %s, expected %s", input, resultEnd, expectedEnd)
	}
}

const minuteSeconds = 60
const hourSeconds = 60 * minuteSeconds

func TestTimeParser(t *testing.T) {
	/*
	 * well-formed
	 */

	testSingleTime(t, "T3", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday},
		StartSeconds: 15 * hourSeconds,
		EndSeconds:   16 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "W3.30", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday},
		StartSeconds: (15 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (16 * hourSeconds) + (30 * minuteSeconds),
	}, nil, nil)
	testSingleTime(t, "R1.30-3.30", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Thursday},
		StartSeconds: (13 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (15 * hourSeconds) + (30 * minuteSeconds),
	}, nil, nil)
	testSingleTime(t, "S4", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Saturday},
		StartSeconds: 16 * hourSeconds,
		EndSeconds:   17 * hourSeconds,
	}, nil, nil)

	testSingleTime(t, "MTF11", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Tuesday, time.Friday},
		StartSeconds: 11 * hourSeconds,
		EndSeconds:   12 * hourSeconds,
	}, nil, nil)

	testSingleTime(t, "TR10.30-12 (BEGINS OCT 21)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: (10 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   12 * hourSeconds,
	}, termDate(2020, time.October, 21), nil)

	testSingleTime(t, "RF11.30 (ENDS DEC 2)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Thursday, time.Friday},
		StartSeconds: (11 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (12 * hourSeconds) + (30 * minuteSeconds),
	}, nil, termDate(2020, time.December, 2))

	testSingleTime(t, "MT9 (MEETS 9/4 TO 10/6)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Tuesday},
		StartSeconds: 9 * hourSeconds,
		EndSeconds:   10 * hourSeconds,
	}, termDate(2020, time.September, 4), termDate(2020, time.October, 6))

	testSingleTime(t, "WF EVE (5-7)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday, time.Friday},
		StartSeconds: 17 * hourSeconds,
		EndSeconds:   19 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "TR EVE (4-6 PM)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: 16 * hourSeconds,
		EndSeconds:   18 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "MW EVE (4.30-5.30 PM) (BEGINS NOV 2)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Wednesday},
		StartSeconds: (16 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (17 * hourSeconds) + (30 * minuteSeconds),
	}, termDate(2020, time.November, 2), nil)

	testSingleTime(t, "MW10 (LIMITED)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Monday, time.Wednesday},
		StartSeconds: 10 * hourSeconds,
		EndSeconds:   11 * hourSeconds,
	}, nil, nil)

	testSingleTime(t, "WR3.30-4.45 (LIMITED) (BEGINS OCT 14) (ENDS OCT 23)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday, time.Thursday},
		StartSeconds: (15 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (16 * hourSeconds) + (45 * minuteSeconds),
	}, termDate(2020, time.October, 14), termDate(2020, time.October, 23))

	testSingleTime(t, "WR EVE (3.30-4.45 PM) (LIMITED) (BEGINS OCT 14) (ENDS OCT 23)", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday, time.Thursday},
		StartSeconds: (15 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (16 * hourSeconds) + (45 * minuteSeconds),
	}, termDate(2020, time.October, 14), termDate(2020, time.October, 23))

	/*
	 * not well-formed
	 */

	testSingleTime(t, "W 4:30", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Wednesday},
		StartSeconds: (16 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (17 * hourSeconds) + (30 * minuteSeconds),
	}, nil, nil)
	testSingleTime(t, "TR 10:30-12p", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: (10 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   12 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "TTH 10:30 - 12 PM", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Tuesday, time.Thursday},
		StartSeconds: (10 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   12 * hourSeconds,
	}, nil, nil)
	testSingleTime(t, "Th 4:30", &ScheduledMeeting{
		Weekdays:     []time.Weekday{time.Thursday},
		StartSeconds: (16 * hourSeconds) + (30 * minuteSeconds),
		EndSeconds:   (17 * hourSeconds) + (30 * minuteSeconds),
	}, nil, nil)
}
<|endoftext|>


<commit_before>// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo

// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file contains code related to the Message struct

package discordgo

import (
	"fmt"
	"io"
	"regexp"
)

// A Message stores all data related to a specific Discord message.
type Message struct {
	ID              string               `json:"id"`
	ChannelID       string               `json:"channel_id"`
	Content         string               `json:"content"`
	Timestamp       Timestamp            `json:"timestamp"`
	EditedTimestamp Timestamp            `json:"edited_timestamp"`
	MentionRoles    []string             `json:"mention_roles"`
	Tts             bool                 `json:"tts"`
	MentionEveryone bool                 `json:"mention_everyone"`
	Author          *User                `json:"author"`
	Attachments     []*MessageAttachment `json:"attachments"`
	Embeds          []*MessageEmbed      `json:"embeds"`
	Mentions        []*User              `json:"mentions"`
	Reactions       []*MessageReactions  `json:"reactions"`
}

// File stores info about files you e.g. send in messages.
type File struct {
	Name   string
	Reader io.Reader
}

// MessageSend stores all parameters you can send with ChannelMessageSendComplex.
type MessageSend struct {
	Content string        `json:"content,omitempty"`
	Embed   *MessageEmbed `json:"embed,omitempty"`
	Tts     bool          `json:"tts"`
	File    *File         `json:"file"`
}

// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
// is also where you should get the instance from.
type MessageEdit struct {
	Content *string       `json:"content,omitempty"`
	Embed   *MessageEmbed `json:"embed,omitempty"`

	ID      string
	Channel string
}

// NewMessageEdit returns a MessageEdit struct, initialized
// with the Channel and ID.
func NewMessageEdit(channelID string, messageID string) *MessageEdit {
	return &MessageEdit{
		Channel: channelID,
		ID:      messageID,
	}
}

// SetContent is the same as setting the variable Content,
// except it doesn't take a pointer.
func (m *MessageEdit) SetContent(str string) *MessageEdit {
	m.Content = &str
	return m
}

// SetEmbed is a convenience function for setting the embed,
// so you can chain commands.
func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit {
	m.Embed = embed
	return m
}

// A MessageAttachment stores data for message attachments.
type MessageAttachment struct {
	ID       string `json:"id"`
	URL      string `json:"url"`
	ProxyURL string `json:"proxy_url"`
	Filename string `json:"filename"`
	Width    int    `json:"width"`
	Height   int    `json:"height"`
	Size     int    `json:"size"`
}

// MessageEmbedFooter is a part of a MessageEmbed struct.
type MessageEmbedFooter struct {
	Text         string `json:"text,omitempty"`
	IconURL      string `json:"icon_url,omitempty"`
	ProxyIconURL string `json:"proxy_icon_url,omitempty"`
}

// MessageEmbedImage is a part of a MessageEmbed struct.
type MessageEmbedImage struct {
	URL      string `json:"url,omitempty"`
	ProxyURL string `json:"proxy_url,omitempty"`
	Width    int    `json:"width,omitempty"`
	Height   int    `json:"height,omitempty"`
}

// MessageEmbedThumbnail is a part of a MessageEmbed struct.
type MessageEmbedThumbnail struct {
	URL      string `json:"url,omitempty"`
	ProxyURL string `json:"proxy_url,omitempty"`
	Width    int    `json:"width,omitempty"`
	Height   int    `json:"height,omitempty"`
}

// MessageEmbedVideo is a part of a MessageEmbed struct.
type MessageEmbedVideo struct {
	URL      string `json:"url,omitempty"`
	ProxyURL string `json:"proxy_url,omitempty"`
	Width    int    `json:"width,omitempty"`
	Height   int    `json:"height,omitempty"`
}

// MessageEmbedProvider is a part of a MessageEmbed struct.
type MessageEmbedProvider struct {
	URL  string `json:"url,omitempty"`
	Name string `json:"name,omitempty"`
}

// MessageEmbedAuthor is a part of a MessageEmbed struct.
type MessageEmbedAuthor struct {
	URL          string `json:"url,omitempty"`
	Name         string `json:"name,omitempty"`
	IconURL      string `json:"icon_url,omitempty"`
	ProxyIconURL string `json:"proxy_icon_url,omitempty"`
}

// MessageEmbedField is a part of a MessageEmbed struct.
type MessageEmbedField struct {
	Name   string `json:"name,omitempty"`
	Value  string `json:"value,omitempty"`
	Inline bool   `json:"inline,omitempty"`
}

// An MessageEmbed stores data for message embeds.
type MessageEmbed struct {
	URL         string                 `json:"url,omitempty"`
	Type        string                 `json:"type,omitempty"`
	Title       string                 `json:"title,omitempty"`
	Description string                 `json:"description,omitempty"`
	Timestamp   string                 `json:"timestamp,omitempty"`
	Color       int                    `json:"color,omitempty"`
	Footer      *MessageEmbedFooter    `json:"footer,omitempty"`
	Image       *MessageEmbedImage     `json:"image,omitempty"`
	Thumbnail   *MessageEmbedThumbnail `json:"thumbnail,omitempty"`
	Video       *MessageEmbedVideo     `json:"video,omitempty"`
	Provider    *MessageEmbedProvider  `json:"provider,omitempty"`
	Author      *MessageEmbedAuthor    `json:"author,omitempty"`
	Fields      []*MessageEmbedField   `json:"fields,omitempty"`
}

// MessageReactions holds a reactions object for a message.
type MessageReactions struct {
	Count int    `json:"count"`
	Me    bool   `json:"me"`
	Emoji *Emoji `json:"emoji"`
}

// ContentWithMentionsReplaced will replace all @<id> mentions with the
// username of the mention.
func (m *Message) ContentWithMentionsReplaced() string {
	if m.Mentions == nil {
		return m.Content
	}
	content := m.Content
	for _, user := range m.Mentions {
		content = regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", user.ID)).ReplaceAllString(content, "@"+user.Username)
	}
	return content
}
<commit_msg>DIE, PRINTF. DIE!<commit_after>// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo

// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file contains code related to the Message struct

package discordgo

import (
	"io"
	"regexp"
)

// A Message stores all data related to a specific Discord message.
type Message struct {
	ID              string               `json:"id"`
	ChannelID       string               `json:"channel_id"`
	Content         string               `json:"content"`
	Timestamp       Timestamp            `json:"timestamp"`
	EditedTimestamp Timestamp            `json:"edited_timestamp"`
	MentionRoles    []string             `json:"mention_roles"`
	Tts             bool                 `json:"tts"`
	MentionEveryone bool                 `json:"mention_everyone"`
	Author          *User                `json:"author"`
	Attachments     []*MessageAttachment `json:"attachments"`
	Embeds          []*MessageEmbed      `json:"embeds"`
	Mentions        []*User              `json:"mentions"`
	Reactions       []*MessageReactions  `json:"reactions"`
}

// File stores info about files you e.g. send in messages.
type File struct {
	Name   string
	Reader io.Reader
}

// MessageSend stores all parameters you can send with ChannelMessageSendComplex.
type MessageSend struct {
	Content string        `json:"content,omitempty"`
	Embed   *MessageEmbed `json:"embed,omitempty"`
	Tts     bool          `json:"tts"`
	File    *File         `json:"file"`
}

// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
// is also where you should get the instance from.
type MessageEdit struct {
	Content *string       `json:"content,omitempty"`
	Embed   *MessageEmbed `json:"embed,omitempty"`

	ID      string
	Channel string
}

// NewMessageEdit returns a MessageEdit struct, initialized
// with the Channel and ID.
func NewMessageEdit(channelID string, messageID string) *MessageEdit {
	return &MessageEdit{
		Channel: channelID,
		ID:      messageID,
	}
}

// SetContent is the same as setting the variable Content,
// except it doesn't take a pointer.
func (m *MessageEdit) SetContent(str string) *MessageEdit {
	m.Content = &str
	return m
}

// SetEmbed is a convenience function for setting the embed,
// so you can chain commands.
func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit {
	m.Embed = embed
	return m
}

// A MessageAttachment stores data for message attachments.
type MessageAttachment struct {
	ID       string `json:"id"`
	URL      string `json:"url"`
	ProxyURL string `json:"proxy_url"`
	Filename string `json:"filename"`
	Width    int    `json:"width"`
	Height   int    `json:"height"`
	Size     int    `json:"size"`
}

// MessageEmbedFooter is a part of a MessageEmbed struct.
type MessageEmbedFooter struct {
	Text         string `json:"text,omitempty"`
	IconURL      string `json:"icon_url,omitempty"`
	ProxyIconURL string `json:"proxy_icon_url,omitempty"`
}

// MessageEmbedImage is a part of a MessageEmbed struct.
type MessageEmbedImage struct {
	URL      string `json:"url,omitempty"`
	ProxyURL string `json:"proxy_url,omitempty"`
	Width    int    `json:"width,omitempty"`
	Height   int    `json:"height,omitempty"`
}

// MessageEmbedThumbnail is a part of a MessageEmbed struct.
type MessageEmbedThumbnail struct {
	URL      string `json:"url,omitempty"`
	ProxyURL string `json:"proxy_url,omitempty"`
	Width    int    `json:"width,omitempty"`
	Height   int    `json:"height,omitempty"`
}

// MessageEmbedVideo is a part of a MessageEmbed struct.
type MessageEmbedVideo struct {
	URL      string `json:"url,omitempty"`
	ProxyURL string `json:"proxy_url,omitempty"`
	Width    int    `json:"width,omitempty"`
	Height   int    `json:"height,omitempty"`
}

// MessageEmbedProvider is a part of a MessageEmbed struct.
type MessageEmbedProvider struct {
	URL  string `json:"url,omitempty"`
	Name string `json:"name,omitempty"`
}

// MessageEmbedAuthor is a part of a MessageEmbed struct.
type MessageEmbedAuthor struct {
	URL          string `json:"url,omitempty"`
	Name         string `json:"name,omitempty"`
	IconURL      string `json:"icon_url,omitempty"`
	ProxyIconURL string `json:"proxy_icon_url,omitempty"`
}

// MessageEmbedField is a part of a MessageEmbed struct.
type MessageEmbedField struct {
	Name   string `json:"name,omitempty"`
	Value  string `json:"value,omitempty"`
	Inline bool   `json:"inline,omitempty"`
}

// An MessageEmbed stores data for message embeds.
type MessageEmbed struct {
	URL         string                 `json:"url,omitempty"`
	Type        string                 `json:"type,omitempty"`
	Title       string                 `json:"title,omitempty"`
	Description string                 `json:"description,omitempty"`
	Timestamp   string                 `json:"timestamp,omitempty"`
	Color       int                    `json:"color,omitempty"`
	Footer      *MessageEmbedFooter    `json:"footer,omitempty"`
	Image       *MessageEmbedImage     `json:"image,omitempty"`
	Thumbnail   *MessageEmbedThumbnail `json:"thumbnail,omitempty"`
	Video       *MessageEmbedVideo     `json:"video,omitempty"`
	Provider    *MessageEmbedProvider  `json:"provider,omitempty"`
	Author      *MessageEmbedAuthor    `json:"author,omitempty"`
	Fields      []*MessageEmbedField   `json:"fields,omitempty"`
}

// MessageReactions holds a reactions object for a message.
type MessageReactions struct {
	Count int    `json:"count"`
	Me    bool   `json:"me"`
	Emoji *Emoji `json:"emoji"`
}

// ContentWithMentionsReplaced will replace all @<id> mentions with the
// username of the mention.
func (m *Message) ContentWithMentionsReplaced() string {
	if m.Mentions == nil {
		return m.Content
	}
	content := m.Content
	for _, user := range m.Mentions {
		content = regexp.MustCompile("<@!?("+regexp.QuoteMeta(user.ID)+")>").ReplaceAllString(content, "@"+user.Username)
	}
	return content
}
<|endoftext|>