<commit_before>package network

import (
	"bytes"
	"context"
	"runtime"
	"strings"
	"sync"
	"time"

	cluster "github.com/bsm/sarama-cluster"
	"github.com/inconshreveable/log15"
	dto "github.com/prometheus/client_model/go"
	"github.com/stephane-martin/skewer/conf"
	"github.com/stephane-martin/skewer/decoders"
	"github.com/stephane-martin/skewer/model"
	"github.com/stephane-martin/skewer/services/base"
	"github.com/stephane-martin/skewer/utils"
	"github.com/stephane-martin/skewer/utils/eerrors"
	"github.com/stephane-martin/skewer/utils/queue"
	"github.com/stephane-martin/skewer/utils/queue/kafka"
)

func initKafkaRegistry() {
	base.Once.Do(func() {
		base.InitRegistry()
	})
}

type KafkaServiceImpl struct {
	configs          []conf.KafkaSourceConfig
	parserConfigs    []conf.ParserConfig
	parserEnv        *decoders.ParsersEnv
	reporter         base.Stasher
	rawMessagesQueue *kafka.Ring
	MaxMessageSize   int
	logger           log15.Logger
	wg               sync.WaitGroup
	stopCtx          context.Context
	stop             context.CancelFunc
	rawpool          *sync.Pool
	queues           *queue.KafkaQueues
	fatalErrorChan   chan struct{}
	fatalOnce        *sync.Once
	confined         bool
}

func NewKafkaService(env *base.ProviderEnv) (base.Provider, error) {
	initKafkaRegistry()
	s := KafkaServiceImpl{
		reporter: env.Reporter,
		logger:   env.Logger.New("class", "KafkaService"),
		confined: env.Confined,
	}
	return &s, nil
}

func (s *KafkaServiceImpl) Type() base.Types {
	return base.KafkaSource
}

func (s *KafkaServiceImpl) SetConf(c conf.BaseConfig) {
	s.rawpool = &sync.Pool{New: func() interface{} {
		return &model.RawKafkaMessage{}
	}}
	s.configs = c.KafkaSource
	s.parserConfigs = c.Parsers
	s.parserEnv = decoders.NewParsersEnv(s.parserConfigs, s.logger)
	s.rawMessagesQueue = kafka.NewRing(c.Main.InputQueueSize)
}

func (s *KafkaServiceImpl) Gather() ([]*dto.MetricFamily, error) {
	return base.Registry.Gather()
}

func (s *KafkaServiceImpl) Start() (infos []model.ListenerInfo, err error) {
	infos = []model.ListenerInfo{}
	s.queues = queue.NewQueueFactory()
	s.stopCtx, s.stop = context.WithCancel(context.Background())
	s.fatalErrorChan = make(chan struct{})
	s.fatalOnce = &sync.Once{}

	// start the workers that fetch messages from the kafka cluster
	// workers push raw messages from kafka to the rawMessagesQueue
	var workersWG sync.WaitGroup
	for _, config := range s.configs {
		workersWG.Add(1)
		go func() {
			defer workersWG.Done()
			s.startWorker(s.stopCtx, config)
		}()
	}

	// when all workers have returned, we know that we won't receive any more raw messages
	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		workersWG.Wait()
		s.rawMessagesQueue.Dispose()
	}()

	// start the parsers that consume raw messages from the rawMessagesQueue
	cpus := runtime.NumCPU()
	for i := 0; i < cpus; i++ {
		s.wg.Add(1)
		go func() {
			defer s.wg.Done()
			err := s.parse()
			if err != nil {
				s.logger.Error(err.Error())
				s.dofatal()
			}
		}()
	}

	return infos, nil
}

func (s *KafkaServiceImpl) FatalError() chan struct{} {
	return s.fatalErrorChan
}

func (s *KafkaServiceImpl) dofatal() {
	s.fatalOnce.Do(func() { close(s.fatalErrorChan) })
}

func getConsumer(ctx context.Context, logger log15.Logger, config conf.KafkaSourceConfig, confined bool) *cluster.Consumer {
	for {
		select {
		case <-ctx.Done():
			return nil
		default:
		}
		consumer, err := config.GetClient(confined)
		if err == nil {
			return consumer
		}
		logger.Debug("Error getting a Kafka consumer", "error", err)
		select {
		case <-ctx.Done():
			return nil
		case <-time.After(2 * time.Second):
		}
	}
}

func (s *KafkaServiceImpl) startWorker(ctx context.Context, config conf.KafkaSourceConfig) {
	// the for loop here makes us retry if we lose a connection to the kafka cluster
	for {
		consumer := getConsumer(ctx, s.logger, config, s.confined)
		if consumer == nil {
			return
		}
		s.handleConsumer(ctx, config, consumer)
	}
}

func (s *KafkaServiceImpl) parse() (err error) {
	for {
		raw, err := s.rawMessagesQueue.Get()
		if raw == nil || err != nil {
			return nil
		}
		err = s.parseOne(raw)
		if err != nil {
			base.ParsingErrorCounter.WithLabelValues("kafka", raw.Client, raw.Decoder.Format).Inc()
			logg(s.logger, &raw.RawMessage).Warn(err.Error())
			if eerrors.IsFatal(err) {
				s.rawpool.Put(raw)
				return err
			}
		}

		// ack the raw message to the kafka cluster
		ackQueue := s.queues.Get(raw.ConsumerID)
		if ackQueue != nil {
			_ = ackQueue.Put(queue.KafkaProducerAck{
				Offset: raw.Offset,
				TopicPartition: queue.TopicPartition{
					Partition: raw.Partition,
					Topic:     raw.Topic,
				},
			})
		}
		s.rawpool.Put(raw)
	}
}

func (s *KafkaServiceImpl) parseOne(raw *model.RawKafkaMessage) (err error) {
	parser, err := s.parserEnv.GetParser(&raw.Decoder)
	if parser == nil || err != nil {
		return decoders.DecodingError(eerrors.Wrapf(err, "Unknown decoder: %s", raw.Decoder.Format))
	}
	defer parser.Release()

	syslogMsgs, err := parser.Parse(raw.Message)
	if err != nil {
		return decoders.DecodingError(eerrors.Wrap(err, "Parsing error"))
	}

	for _, syslogMsg := range syslogMsgs {
		if syslogMsg == nil {
			continue
		}
		full := model.FullFactoryFrom(syslogMsg)
		full.Uid = raw.UID
		full.ConfId = raw.ConfID
		full.SourceType = "kafka"
		full.ClientAddr = raw.Client
		err := s.reporter.Stash(full)
		model.FullFree(full)

		if err != nil {
			logg(s.logger, &raw.RawMessage).Warn("Error stashing Kafka message", "error", err)
			if eerrors.IsFatal(err) {
				return eerrors.Wrap(err, "Fatal error pushing Kafka message to the Store")
			}
		}
	}
	return nil
}

func (s *KafkaServiceImpl) Shutdown() {
	s.Stop()
}

func (s *KafkaServiceImpl) Stop() {
	s.stop()
	// the call to s.stop() makes every worker to return eventually
	// when every worker has returned, rawMessagesQueue is disposed
	// because of that, the parsers will return too
	s.wg.Wait()
}

func (s *KafkaServiceImpl) handleConsumer(ctx context.Context, config conf.KafkaSourceConfig, consumer *cluster.Consumer) {
	if consumer == nil {
		s.logger.Error("BUG: the consumer passed to handleConsumer is NIL")
		return
	}
	var wg sync.WaitGroup
	lctx, lcancel := context.WithCancel(ctx)
	ackQueue := s.queues.New()

	go func() {
		<-lctx.Done()
		consumer.Close()
	}()

	wg.Add(1)
	// ack messages to kafka when needed
	// the goroutine returns eventually after the consumer has been closed
	go func() {
		defer wg.Done()
		processedMsgs := map[queue.TopicPartition](map[int64]bool){}
		nextToACK := map[queue.TopicPartition]int64{}

		for ackQueue.Wait() {
			ack, err := ackQueue.Get()
			if err != nil {
				return
			}
			if len(ack.Topic) == 0 {
				continue
			}
			// a little dance to ACK kafka messages in growing order for each partition
			if _, ok := processedMsgs[ack.TopicPartition]; !ok {
				processedMsgs[ack.TopicPartition] = map[int64]bool{}
			}
			processedMsgs[ack.TopicPartition][ack.Offset] = true
			next, ok := nextToACK[ack.TopicPartition]
			if !ok {
				next = ack.Offset
			}
			for processedMsgs[ack.TopicPartition][next] {
				delete(processedMsgs[ack.TopicPartition], next)
				consumer.MarkPartitionOffset(ack.Topic, ack.Partition, next, "")
				next++
				nextToACK[ack.TopicPartition] = next
			}
		}
	}()

	wg.Add(1)
	// watch kafka errors
	// the goroutine returns eventually after the consumer has been closed
	go func() {
		defer wg.Done()
		for err := range consumer.Errors() {
			if model.IsFatalKafkaError(err) {
				s.logger.Warn("Kafka consumer fatal error", "error", err)
				consumer.Close()
			} else {
				s.logger.Info("Kafka consumer non fatal error", "error", err)
			}
		}
	}()

	wg.Add(1)
	// watch kafka messages
	// the goroutine returns eventually after the consumer has been closed
	go func() {
		defer wg.Done()
		gen := utils.NewGenerator()
		brokers := strings.Join(config.Brokers, ",")

	Loop:
		for msg := range consumer.Messages() {
			ok := true
			value := bytes.TrimSpace(msg.Value)
			if len(value) == 0 {
				s.logger.Warn("Empty message")
				ok = false
			}
			if s.MaxMessageSize > 0 && len(value) > s.MaxMessageSize {
				s.logger.Warn("Message too large")
				ok = false
			}
			if !ok {
				// if the message is rejected, immediately ACK it to Kafka
				ackQueue.Put(
					queue.KafkaProducerAck{
						Offset: msg.Offset,
						TopicPartition: queue.TopicPartition{
							Partition: msg.Partition,
							Topic:     msg.Topic,
						},
					},
				)
				continue Loop
			}
			raw := s.rawpool.Get().(*model.RawKafkaMessage)
			raw.UID = gen.Uid()
			raw.Client = brokers
			raw.ConfID = config.ConfID
			raw.ConsumerID = ackQueue.ID()
			raw.Decoder = config.DecoderBaseConfig
			raw.Message = raw.Message[:len(value)]
			copy(raw.Message, value)
			raw.Topic = msg.Topic
			raw.Partition = msg.Partition
			raw.Offset = msg.Offset
			err := s.rawMessagesQueue.Put(raw)
			if err != nil {
				// rawMessagesQueue has been disposed
				s.rawpool.Put(raw)
				s.logger.Warn("Error queueing kafka message", "error", err)
			} else {
				base.IncomingMsgsCounter.WithLabelValues("kafka", raw.Client, "", "").Inc()
			}
		}

		// the previous for loop returns when the Messages channel has been closed
		// the Messages channel is only closed after the consumer has been closed
		// so here we now that the current kafka consumer is gone
		// hence, there is no need to process ACK any further
		s.queues.Delete(ackQueue)

	}()

	wg.Wait()
	lcancel()
}
<commit_msg>use a circuit breaker to make the initial connection to kafka in the kafka sources<commit_after>package network

import (
	"bytes"
	"context"
	"runtime"
	"strings"
	"sync"
	"time"

	cluster "github.com/bsm/sarama-cluster"
	"github.com/inconshreveable/log15"
	dto "github.com/prometheus/client_model/go"
	circuit "github.com/rubyist/circuitbreaker"
	"github.com/stephane-martin/skewer/conf"
	"github.com/stephane-martin/skewer/decoders"
	"github.com/stephane-martin/skewer/model"
	"github.com/stephane-martin/skewer/services/base"
	"github.com/stephane-martin/skewer/utils"
	"github.com/stephane-martin/skewer/utils/eerrors"
	"github.com/stephane-martin/skewer/utils/queue"
	"github.com/stephane-martin/skewer/utils/queue/kafka"
)

func initKafkaRegistry() {
	base.Once.Do(func() {
		base.InitRegistry()
	})
}

type KafkaServiceImpl struct {
	configs          []conf.KafkaSourceConfig
	parserConfigs    []conf.ParserConfig
	parserEnv        *decoders.ParsersEnv
	reporter         base.Stasher
	rawMessagesQueue *kafka.Ring
	MaxMessageSize   int
	logger           log15.Logger
	wg               sync.WaitGroup
	stopCtx          context.Context
	stop             context.CancelFunc
	rawpool          *sync.Pool
	queues           *queue.KafkaQueues
	fatalErrorChan   chan struct{}
	fatalOnce        *sync.Once
	confined         bool
}

func NewKafkaService(env *base.ProviderEnv) (base.Provider, error) {
	initKafkaRegistry()
	s := KafkaServiceImpl{
		reporter: env.Reporter,
		logger:   env.Logger.New("class", "KafkaService"),
		confined: env.Confined,
	}
	return &s, nil
}

func (s *KafkaServiceImpl) Type() base.Types {
	return base.KafkaSource
}

func (s *KafkaServiceImpl) SetConf(c conf.BaseConfig) {
	s.rawpool = &sync.Pool{New: func() interface{} {
		return &model.RawKafkaMessage{}
	}}
	s.configs = c.KafkaSource
	s.parserConfigs = c.Parsers
	s.parserEnv = decoders.NewParsersEnv(s.parserConfigs, s.logger)
	s.rawMessagesQueue = kafka.NewRing(c.Main.InputQueueSize)
}

func (s *KafkaServiceImpl) Gather() ([]*dto.MetricFamily, error) {
	return base.Registry.Gather()
}

func (s *KafkaServiceImpl) Start() (infos []model.ListenerInfo, err error) {
	infos = []model.ListenerInfo{}
	s.queues = queue.NewQueueFactory()
	s.stopCtx, s.stop = context.WithCancel(context.Background())
	s.fatalErrorChan = make(chan struct{})
	s.fatalOnce = &sync.Once{}

	// start the workers that fetch messages from the kafka cluster
	// workers push raw messages from kafka to the rawMessagesQueue
	var workersWG sync.WaitGroup
	for _, config := range s.configs {
		workersWG.Add(1)
		go func() {
			defer workersWG.Done()
			s.startWorker(s.stopCtx, config)
		}()
	}

	// when all workers have returned, we know that we won't receive any more raw messages
	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		workersWG.Wait()
		s.rawMessagesQueue.Dispose()
	}()

	// start the parsers that consume raw messages from the rawMessagesQueue
	cpus := runtime.NumCPU()
	for i := 0; i < cpus; i++ {
		s.wg.Add(1)
		go func() {
			defer s.wg.Done()
			err := s.parse()
			if err != nil {
				s.logger.Error(err.Error())
				s.dofatal()
			}
		}()
	}

	return infos, nil
}

func (s *KafkaServiceImpl) FatalError() chan struct{} {
	return s.fatalErrorChan
}

func (s *KafkaServiceImpl) dofatal() {
	s.fatalOnce.Do(func() { close(s.fatalErrorChan) })
}

func (s *KafkaServiceImpl) startWorker(ctx context.Context, config conf.KafkaSourceConfig) {
	breaker := circuit.NewConsecutiveBreaker(3)

	getConsumer := func() (consumer *cluster.Consumer) {

		getClient := func() (err error) {
			consumer, err = config.GetClient(s.confined)
			return err
		}

		for {
			// arg timeout is 0 because the sarama client handles connection timeout
			// through DialTimeout
			err := breaker.CallContext(ctx, getClient, 0)

			if err == nil {
				return consumer
			}
			if err == context.Canceled {
				return nil
			}
			if err != circuit.ErrBreakerOpen {
				s.logger.Debug("Error getting a Kafka consumer", "error", err)
			}

			select {
			case <-ctx.Done():
				return nil
			case <-time.After(1 * time.Second):
			}
		}
	}

	// the for loop here makes us retry if we lose a connection to the kafka cluster
	for {
		consumer := getConsumer()
		if consumer == nil {
			return
		}
		s.handleConsumer(ctx, config, consumer)
	}
}

func (s *KafkaServiceImpl) parse() (err error) {
	for {
		raw, err := s.rawMessagesQueue.Get()
		if raw == nil || err != nil {
			return nil
		}
		err = s.parseOne(raw)
		if err != nil {
			base.ParsingErrorCounter.WithLabelValues("kafka", raw.Client, raw.Decoder.Format).Inc()
			logg(s.logger, &raw.RawMessage).Warn(err.Error())
			if eerrors.IsFatal(err) {
				s.rawpool.Put(raw)
				return err
			}
		}

		// ack the raw message to the kafka cluster
		ackQueue := s.queues.Get(raw.ConsumerID)
		if ackQueue != nil {
			_ = ackQueue.Put(queue.KafkaProducerAck{
				Offset: raw.Offset,
				TopicPartition: queue.TopicPartition{
					Partition: raw.Partition,
					Topic:     raw.Topic,
				},
			})
		}
		s.rawpool.Put(raw)
	}
}

func (s *KafkaServiceImpl) parseOne(raw *model.RawKafkaMessage) (err error) {
	parser, err := s.parserEnv.GetParser(&raw.Decoder)
	if parser == nil || err != nil {
		return decoders.DecodingError(eerrors.Wrapf(err, "Unknown decoder: %s", raw.Decoder.Format))
	}
	defer parser.Release()

	syslogMsgs, err := parser.Parse(raw.Message)
	if err != nil {
		return decoders.DecodingError(eerrors.Wrap(err, "Parsing error"))
	}

	for _, syslogMsg := range syslogMsgs {
		if syslogMsg == nil {
			continue
		}
		full := model.FullFactoryFrom(syslogMsg)
		full.Uid = raw.UID
		full.ConfId = raw.ConfID
		full.SourceType = "kafka"
		full.ClientAddr = raw.Client
		err := s.reporter.Stash(full)
		model.FullFree(full)

		if err != nil {
			logg(s.logger, &raw.RawMessage).Warn("Error stashing Kafka message", "error", err)
			if eerrors.IsFatal(err) {
				return eerrors.Wrap(err, "Fatal error pushing Kafka message to the Store")
			}
		}
	}
	return nil
}

func (s *KafkaServiceImpl) Shutdown() {
	s.Stop()
}

func (s *KafkaServiceImpl) Stop() {
	s.stop()
	// the call to s.stop() makes every worker to return eventually
	// when every worker has returned, rawMessagesQueue is disposed
	// because of that, the parsers will return too
	s.wg.Wait()
}

func (s *KafkaServiceImpl) handleConsumer(ctx context.Context, config conf.KafkaSourceConfig, consumer *cluster.Consumer) {
	if consumer == nil {
		s.logger.Error("BUG: the consumer passed to handleConsumer is NIL")
		return
	}
	var wg sync.WaitGroup
	lctx, lcancel := context.WithCancel(ctx)
	ackQueue := s.queues.New()

	go func() {
		<-lctx.Done()
		consumer.Close()
	}()

	wg.Add(1)
	// ack messages to kafka when needed
	// the goroutine returns eventually after the consumer has been closed
	go func() {
		defer wg.Done()
		processedMsgs := map[queue.TopicPartition](map[int64]bool){}
		nextToACK := map[queue.TopicPartition]int64{}

		for ackQueue.Wait() {
			ack, err := ackQueue.Get()
			if err != nil {
				return
			}
			if len(ack.Topic) == 0 {
				continue
			}
			// a little dance to ACK kafka messages in growing order for each partition
			if _, ok := processedMsgs[ack.TopicPartition]; !ok {
				processedMsgs[ack.TopicPartition] = map[int64]bool{}
			}
			processedMsgs[ack.TopicPartition][ack.Offset] = true
			next, ok := nextToACK[ack.TopicPartition]
			if !ok {
				next = ack.Offset
			}
			for processedMsgs[ack.TopicPartition][next] {
				delete(processedMsgs[ack.TopicPartition], next)
				consumer.MarkPartitionOffset(ack.Topic, ack.Partition, next, "")
				next++
				nextToACK[ack.TopicPartition] = next
			}
		}
	}()

	wg.Add(1)
	// watch kafka errors
	// the goroutine returns eventually after the consumer has been closed
	go func() {
		defer wg.Done()
		for err := range consumer.Errors() {
			if model.IsFatalKafkaError(err) {
				s.logger.Warn("Kafka consumer fatal error", "error", err)
				consumer.Close()
			} else {
				s.logger.Info("Kafka consumer non fatal error", "error", err)
			}
		}
	}()

	wg.Add(1)
	// watch kafka messages
	// the goroutine returns eventually after the consumer has been closed
	go func() {
		defer wg.Done()
		gen := utils.NewGenerator()
		brokers := strings.Join(config.Brokers, ",")

	Loop:
		for msg := range consumer.Messages() {
			ok := true
			value := bytes.TrimSpace(msg.Value)
			if len(value) == 0 {
				s.logger.Warn("Empty message")
				ok = false
			}
			if s.MaxMessageSize > 0 && len(value) > s.MaxMessageSize {
				s.logger.Warn("Message too large")
				ok = false
			}
			if !ok {
				// if the message is rejected, immediately ACK it to Kafka
				ackQueue.Put(
					queue.KafkaProducerAck{
						Offset: msg.Offset,
						TopicPartition: queue.TopicPartition{
							Partition: msg.Partition,
							Topic:     msg.Topic,
						},
					},
				)
				continue Loop
			}
			raw := s.rawpool.Get().(*model.RawKafkaMessage)
			raw.UID = gen.Uid()
			raw.Client = brokers
			raw.ConfID = config.ConfID
			raw.ConsumerID = ackQueue.ID()
			raw.Decoder = config.DecoderBaseConfig
			raw.Message = raw.Message[:len(value)]
			copy(raw.Message, value)
			raw.Topic = msg.Topic
			raw.Partition = msg.Partition
			raw.Offset = msg.Offset
			err := s.rawMessagesQueue.Put(raw)
			if err != nil {
				// rawMessagesQueue has been disposed
				s.rawpool.Put(raw)
				s.logger.Warn("Error queueing kafka message", "error", err)
			} else {
				base.IncomingMsgsCounter.WithLabelValues("kafka", raw.Client, "", "").Inc()
			}
		}

		// the previous for loop returns when the Messages channel has been closed
		// the Messages channel is only closed after the consumer has been closed
		// so here we now that the current kafka consumer is gone
		// hence, there is no need to process ACK any further
		s.queues.Delete(ackQueue)

	}()

	wg.Wait()
	lcancel()
}
<|endoftext|>


<commit_before>package api_availability

import (
	"testing"
	"time"

	"github.com/cloudfoundry-incubator/cf-test-helpers/cf"
	"github.com/cloudfoundry-incubator/cf-test-helpers/generator"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	. "github.com/onsi/gomega/gexec"
)

var (
	appName        string
	pushTimeout    = 2 * time.Minute
	defaultTimeout = 30 * time.Second
)

var _ = BeforeSuite(func() {
	appName = generator.PrefixedRandomName("API-AVAILABILITY-", "myapp")

	Expect(cf.Cf(
		"push", appName,
		"-p", "../../../example-apps/static-app",
	).Wait(pushTimeout)).To(Exit(0))
})

var _ = AfterSuite(func() {
	cf.Cf("delete", appName, "-f").Wait(defaultTimeout)
})

func TestSuite(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "API Availability test")
}
<commit_msg>Refine creation of api-availability test app.<commit_after>package api_availability

import (
	"testing"
	"time"

	"github.com/cloudfoundry-incubator/cf-test-helpers/cf"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	. "github.com/onsi/gomega/gexec"
)

var (
	appName        string
	pushTimeout    = 2 * time.Minute
	defaultTimeout = 30 * time.Second
)

var _ = BeforeSuite(func() {
	appName = "api-availability-test-app"

	Expect(cf.Cf(
		"push", appName,
		"-p", "../../../example-apps/static-app",
		"-b", "staticfile_buildpack",
		"-m", "64M",
	).Wait(pushTimeout)).To(Exit(0))
})

var _ = AfterSuite(func() {
	cf.Cf("delete", appName, "-f", "-r").Wait(defaultTimeout)
})

func TestSuite(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "API Availability test")
}
<|endoftext|>


<commit_before>// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package userhtml

import (
	"context"
	"fmt"
	"strings"

	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/logging"
	"go.chromium.org/luci/server/redisconn"
	"go.chromium.org/luci/server/router"
	"go.chromium.org/luci/server/templates"

	"go.chromium.org/luci/cv/internal/common"
	"go.chromium.org/luci/cv/internal/rpc/admin"
	adminpb "go.chromium.org/luci/cv/internal/rpc/admin/api"
	"go.chromium.org/luci/cv/internal/run"
)

func recentsPage(c *router.Context) {
	project := c.Params.ByName("Project")

	params, err := parseFormParams(c)
	if err != nil {
		errPage(c, err)
		return
	}

	// TODO(crbug/1233963): check if user has permission to search this specific project.
	req := &adminpb.SearchRunsRequest{
		Project:   project,
		PageToken: params.pageToken,
		PageSize:  50,
		Status:    params.status,
		Mode:      string(params.mode),
	}

	adminServer := &admin.AdminServer{}
	resp, err := adminServer.SearchRuns(c.Context, req)
	if err != nil {
		errPage(c, err)
		return
	}
	logging.Debugf(c.Context, "%d runs retrieved", len(resp.Runs))

	prev, next, err := pageTokens(c.Context, "recent-runs", params.pageToken, resp.NextPageToken)
	if err != nil {
		if common.IsDev(c.Context) {
			logging.Warningf(c.Context, "Could not connect to Redis, paging back disabled. %s", err.Error())
			next = resp.NextPageToken
		} else {
			errPage(c, err)
			return
		}
	}

	templates.MustRender(c.Context, c.Writer, "pages/recent_runs.html", map[string]interface{}{
		"Runs":         resp.Runs,
		"Project":      project,
		"PrevPage":     prev,
		"NextPage":     next,
		"FilterStatus": params.statusString(),
		"FilterMode":   params.modeString(),
	})
}

type recentRunsParams struct {
	status    run.Status
	mode      run.Mode
	pageToken string
}

func parseFormParams(c *router.Context) (recentRunsParams, error) {
	params := recentRunsParams{}
	if err := c.Request.ParseForm(); err != nil {
		return params, errors.Annotate(err, "failed to parse form").Err()
	}

	s := c.Request.Form.Get("status")
	switch val, ok := run.Status_value[strings.ToUpper(s)]; {
	case s == "":
	case ok:
		params.status = run.Status(val)
	default:
		return params, fmt.Errorf("invalid Run status %q", s)
	}

	switch m := run.Mode(c.Request.Form.Get("mode")); {
	case m == "":
	case m.Valid():
		params.mode = m
	default:
		return params, fmt.Errorf("invalid Run mode %q", params.mode)
	}

	params.pageToken = strings.TrimSpace(c.Request.Form.Get("page"))
	return params, nil
}

func (r *recentRunsParams) statusString() string {
	if r.status == run.Status_STATUS_UNSPECIFIED {
		return ""
	}
	return r.status.String()
}

func (r *recentRunsParams) modeString() string {
	return string(r.mode)
}

// pageTokens caches the current pageToken associated to the next,
// so as to populate the previous page link when rendering the next page.
func pageTokens(ctx context.Context, namespace, pageToken, nextPageToken string) (prev, next string, err error) {
	if strings.Contains(namespace, "/") {
		panic(fmt.Errorf("namespace %q must not contain slashes", namespace))
	}

	key := func(token string) string {
		return fmt.Sprintf("%s/%s", namespace, token)
	}

	next = nextPageToken
	conn, err := redisconn.Get(ctx)
	if err != nil {
		err = errors.Annotate(err, "failed to connect to Redis").Err()
		return
	}
	defer conn.Close()

	var val interface{}
	if pageToken != "" {
		val, err = conn.Do("GET", key(pageToken))
		if err != nil {
			err = errors.Annotate(err, "failed to GET previous page token").Err()
			return
		}
		if val != nil {
			switch v := val.(type) {
			case string:
				// TODO(robertocn): delete if this case is never useful.
				prev = v
			case []byte:
				prev = string(v)
			default:
				err = errors.Annotate(err, "failed to decode previous page token: %v", val).Err()
				return
			}
			if prev == "NULL" {
				// Hack to get the second page to correctly show a previous
				// page link with no page token.
				prev = " "
			}
		}
	}
	if next != "" {
		if pageToken == "" {
			pageToken = "NULL"
		}
		// Keep the previous token for 24 hours.
		_, err = conn.Do("SET", key(next), pageToken, "EX", 24*60*60)
		if err != nil {
			err = errors.Annotate(err, "failed to SET current page token").Err()
			return
		}
	}
	return
}
<commit_msg>[cv] Make CV UI use server/caching/layered for managing paging tokens.<commit_after>// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package userhtml

import (
	"context"
	"fmt"
	"strings"
	"time"

	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/logging"
	"go.chromium.org/luci/server/caching"
	"go.chromium.org/luci/server/caching/layered"
	"go.chromium.org/luci/server/router"
	"go.chromium.org/luci/server/templates"

	"go.chromium.org/luci/cv/internal/rpc/admin"
	adminpb "go.chromium.org/luci/cv/internal/rpc/admin/api"
	"go.chromium.org/luci/cv/internal/run"
)

func recentsPage(c *router.Context) {
	project := c.Params.ByName("Project")

	params, err := parseFormParams(c)
	if err != nil {
		errPage(c, err)
		return
	}

	// TODO(crbug/1233963): check if user has permission to search this specific project.
	req := &adminpb.SearchRunsRequest{
		Project:   project,
		PageToken: params.pageToken,
		PageSize:  50,
		Status:    params.status,
		Mode:      string(params.mode),
	}

	adminServer := &admin.AdminServer{}
	resp, err := adminServer.SearchRuns(c.Context, req)
	if err != nil {
		errPage(c, err)
		return
	}
	logging.Debugf(c.Context, "%d runs retrieved", len(resp.Runs))

	prev, err := pageTokens(c.Context, params.pageToken, resp.NextPageToken)
	if err != nil {
		errPage(c, err)
		return
	}

	templates.MustRender(c.Context, c.Writer, "pages/recent_runs.html", map[string]interface{}{
		"Runs":         resp.Runs,
		"Project":      project,
		"PrevPage":     prev,
		"NextPage":     resp.NextPageToken,
		"FilterStatus": params.statusString(),
		"FilterMode":   params.modeString(),
	})
}

type recentRunsParams struct {
	status    run.Status
	mode      run.Mode
	pageToken string
}

func parseFormParams(c *router.Context) (recentRunsParams, error) {
	params := recentRunsParams{}
	if err := c.Request.ParseForm(); err != nil {
		return params, errors.Annotate(err, "failed to parse form").Err()
	}

	s := c.Request.Form.Get("status")
	switch val, ok := run.Status_value[strings.ToUpper(s)]; {
	case s == "":
	case ok:
		params.status = run.Status(val)
	default:
		return params, fmt.Errorf("invalid Run status %q", s)
	}

	switch m := run.Mode(c.Request.Form.Get("mode")); {
	case m == "":
	case m.Valid():
		params.mode = m
	default:
		return params, fmt.Errorf("invalid Run mode %q", params.mode)
	}

	params.pageToken = strings.TrimSpace(c.Request.Form.Get("page"))
	return params, nil
}

func (r *recentRunsParams) statusString() string {
	if r.status == run.Status_STATUS_UNSPECIFIED {
		return ""
	}
	return r.status.String()
}

func (r *recentRunsParams) modeString() string {
	return string(r.mode)
}

var tokenCache = layered.Cache{
	ProcessLRUCache: caching.RegisterLRUCache(1024),
	GlobalNamespace: "recent_cv_runs_page_token_cache",
	Marshal: func(item interface{}) ([]byte, error) {
		return []byte(item.(string)), nil
	},
	Unmarshal: func(blob []byte) (interface{}, error) {
		return string(blob), nil
	},
}

var tokenExp = 24 * time.Hour

// pageTokens caches the current pageToken associated to the next,
// so as to populate the previous page link when rendering the next page.
// Also returns a previously saved page token pointing to the previous page.
func pageTokens(ctx context.Context, pageToken, nextPageToken string) (prev string, err error) {

	// A whitespace will cause a 'Previous' link to render with no page token.
	// i.e. going to the first page.
	// An empty string will not render any 'Previous' link.

	// TODO(crbug.com/1249253): Consider redirecting to the first page of the
	// query when the given page token is valid, but we can't retrieve its
	// previous page.
	blankToken := " "
	var cachedV interface{}

	if pageToken == "" {
		pageToken = blankToken
	} else {
		cachedV, err = tokenCache.GetOrCreate(ctx, pageToken, func() (v interface{}, exp time.Duration, err error) {
			// We haven't seen this token yet, we don't know what its previous page is.
			return "", 0, nil
		})
		if err != nil {
			return
		}
		prev = cachedV.(string)
	}
	_, err = tokenCache.GetOrCreate(ctx, nextPageToken, func() (v interface{}, exp time.Duration, err error) {
		return pageToken, tokenExp, nil
	})
	return
}
<|endoftext|>


<commit_before><commit_msg>Fix import order<commit_after><|endoftext|>


<commit_before>// Copyright 2017 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package rpc

import (
	"errors"
	"math/rand"
	"strings"
	"sync"
)

// Remote defines an address or a group of addresses that all point to a remote
// that we can connect to.
type Remote interface {
	// GetAddress gets an address of the Remote to connect to.
	GetAddress() string
	// Peek returns an address of the Remote to connect to without changing the
	// internal state. The returned address is what GetAddress() would return
	// if called.
	Peek() string
	// Reset resets the internal counter so that next call to GetAddress()
	// returns an address as if it were called the first time.
	Reset()
	// String returns a string that represents all addresses in Remote and can
	// be used to construct a Remote that behaves the same way.
	String() string
}

type fixedRemote string

// NewFixedRemote returns a remote that always uses remoteAddr.
func NewFixedRemote(remoteAddr string) Remote {
	return fixedRemote(remoteAddr)
}

// GetAddress implements the Remote interface.
func (r fixedRemote) GetAddress() string {
	return string(r)
}

// Peek implements the Remote interface.
func (r fixedRemote) Peek() string {
	return string(r)
}

// Reset implements the Remote interface.
func (r fixedRemote) Reset() {}

// String implements the Remote interface.
func (r fixedRemote) String() string {
	return string(r)
}

type prioritizedRoundRobinRemote struct {
	addresses [][]string

	lock      sync.Mutex
	toIterate [][]string
}

// NewPrioritizedRoundRobinRemote creates a new Remote that include
// prioritized remote groups. Each call to GetAddress() will round-robin by
// random order within the first group. If we run out of address within the
// first group, fallback to second group and do the same thing, until we've
// iterated all groups where we'll start over from first group.
//
// Any successful connecting attempt should result in a call to Reset(). This
// is generally handled by the rpc package itself and shouldn't be worried
// about by the user of rpc package unless noted otherwise.
func NewPrioritizedRoundRobinRemote(addressGroups [][]string) (Remote, error) {
	// filter out empty ones
	cleaned := make([][]string, 0, len(addressGroups))
	for _, group := range addressGroups {
		cleanedGroup := make([]string, 0, len(group))
		for _, addr := range group {
			addr := strings.ToLower(strings.TrimSpace(addr))
			if len(addr) > 0 {
				cleanedGroup = append(cleanedGroup, addr)
			}
		}
		if len(cleanedGroup) > 0 {
			cleaned = append(cleaned, cleanedGroup)
		}
	}

	if len(cleaned) == 0 {
		return nil, errors.New("addressGroups has no address")
	}

	r := &prioritizedRoundRobinRemote{
		addresses: cleaned,
	}
	r.resetLocked()
	return r, nil
}

func (r *prioritizedRoundRobinRemote) resetLocked() {
	r.toIterate = make([][]string, 0, len(r.addresses))
	for _, group := range r.addresses {
		groupCopied := make([]string, 0, len(group))
		for _, i := range rand.Perm(len(group)) {
			groupCopied = append(groupCopied, group[i])
		}
		r.toIterate = append(r.toIterate, groupCopied)
	}
}

// Reset implements the Remote interface.
func (r *prioritizedRoundRobinRemote) Reset() {
	r.lock.Lock()
	defer r.lock.Unlock()

	r.resetLocked()
}

// GetAddress implements the Remote interface.
func (r *prioritizedRoundRobinRemote) GetAddress() string {
	r.lock.Lock()
	defer r.lock.Unlock()

	// If we have run out of addresses, reset to include all addresses and
	// start over on next call.
	if len(r.toIterate) == 0 {
		r.resetLocked()
	}

	addr := r.toIterate[0][0]

	// Prune one address. Also prune the address group if it's empty.
	r.toIterate[0] = r.toIterate[0][1:]
	for len(r.toIterate) != 0 && len(r.toIterate[0]) == 0 {
		r.toIterate = r.toIterate[1:]
	}

	return addr
}

// Peek implements the Remote interface.
func (r *prioritizedRoundRobinRemote) Peek() string {
	// If we have run out of addresses, reset to include all addresses and
	// start over on next call.
	if len(r.toIterate) == 0 {
		r.resetLocked()
	}

	return r.toIterate[0][0]
}

func (r *prioritizedRoundRobinRemote) String() string {
	addressGroups := make([]string, 0, len(r.addresses))
	for _, group := range r.addresses {
		addressGroups = append(addressGroups, strings.Join(group, ","))
	}
	return strings.Join(addressGroups, ";")
}

// ParsePrioritizedRoundRobinRemote parses a string into a prioritized
// round robin Remote. See doc for NewPrioritizedRoundRobinRemote for details
// on the returned Remote.
//
// Example:
//   "example0.com,example1.com;example0.net,example1.net" produces a
//   prioritized round robin remote with two groups, first .com then .net.
func ParsePrioritizedRoundRobinRemote(str string) (Remote, error) {
	groups := strings.Split(str, ";")
	addressGroups := make([][]string, 0, len(groups))
	for _, group := range groups {
		addressGroups = append(addressGroups, strings.Split(group, ","))
	}
	return NewPrioritizedRoundRobinRemote(addressGroups)
}
<commit_msg>go fmt<commit_after>// Copyright 2017 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package rpc

import (
	"errors"
	"math/rand"
	"strings"
	"sync"
)

// Remote defines an address or a group of addresses that all point to a remote
// that we can connect to.
type Remote interface {
	// GetAddress gets an address of the Remote to connect to.
	GetAddress() string
	// Peek returns an address of the Remote to connect to without changing the
	// internal state. The returned address is what GetAddress() would return
	// if called.
	Peek() string
	// Reset resets the internal counter so that next call to GetAddress()
	// returns an address as if it were called the first time.
	Reset()
	// String returns a string that represents all addresses in Remote and can
	// be used to construct a Remote that behaves the same way.
	String() string
}

type fixedRemote string

// NewFixedRemote returns a remote that always uses remoteAddr.
func NewFixedRemote(remoteAddr string) Remote {
	return fixedRemote(remoteAddr)
}

// GetAddress implements the Remote interface.
func (r fixedRemote) GetAddress() string {
	return string(r)
}

// Peek implements the Remote interface.
func (r fixedRemote) Peek() string {
	return string(r)
}

// Reset implements the Remote interface.
func (r fixedRemote) Reset() {}

// String implements the Remote interface.
func (r fixedRemote) String() string {
	return string(r)
}

type prioritizedRoundRobinRemote struct {
	addresses [][]string

	lock      sync.Mutex
	toIterate [][]string
}

// NewPrioritizedRoundRobinRemote creates a new Remote that include
// prioritized remote groups. Each call to GetAddress() will round-robin by
// random order within the first group. If we run out of address within the
// first group, fallback to second group and do the same thing, until we've
// iterated all groups where we'll start over from first group.
//
// Any successful connecting attempt should result in a call to Reset(). This
// is generally handled by the rpc package itself and shouldn't be worried
// about by the user of rpc package unless noted otherwise.
func NewPrioritizedRoundRobinRemote(addressGroups [][]string) (Remote, error) {
	// filter out empty ones
	cleaned := make([][]string, 0, len(addressGroups))
	for _, group := range addressGroups {
		cleanedGroup := make([]string, 0, len(group))
		for _, addr := range group {
			addr := strings.ToLower(strings.TrimSpace(addr))
			if len(addr) > 0 {
				cleanedGroup = append(cleanedGroup, addr)
			}
		}
		if len(cleanedGroup) > 0 {
			cleaned = append(cleaned, cleanedGroup)
		}
	}

	if len(cleaned) == 0 {
		return nil, errors.New("addressGroups has no address")
	}

	r := &prioritizedRoundRobinRemote{
		addresses: cleaned,
	}
	r.resetLocked()
	return r, nil
}

func (r *prioritizedRoundRobinRemote) resetLocked() {
	r.toIterate = make([][]string, 0, len(r.addresses))
	for _, group := range r.addresses {
		groupCopied := make([]string, 0, len(group))
		for _, i := range rand.Perm(len(group)) {
			groupCopied = append(groupCopied, group[i])
		}
		r.toIterate = append(r.toIterate, groupCopied)
	}
}

// Reset implements the Remote interface.
func (r *prioritizedRoundRobinRemote) Reset() {
	r.lock.Lock()
	defer r.lock.Unlock()

	r.resetLocked()
}

// GetAddress implements the Remote interface.
func (r *prioritizedRoundRobinRemote) GetAddress() string {
	r.lock.Lock()
	defer r.lock.Unlock()

	// If we have run out of addresses, reset to include all addresses and
	// start over on next call.
	if len(r.toIterate) == 0 {
		r.resetLocked()
	}

	addr := r.toIterate[0][0]

	// Prune one address. Also prune the address group if it's empty.
	r.toIterate[0] = r.toIterate[0][1:]
	for len(r.toIterate) != 0 && len(r.toIterate[0]) == 0 {
		r.toIterate = r.toIterate[1:]
	}

	return addr
}

// Peek implements the Remote interface.
func (r *prioritizedRoundRobinRemote) Peek() string {
	// If we have run out of addresses, reset to include all addresses and
	// start over on next call.
	if len(r.toIterate) == 0 {
		r.resetLocked()
	}

	return r.toIterate[0][0]
}

func (r *prioritizedRoundRobinRemote) String() string {
	addressGroups := make([]string, 0, len(r.addresses))
	for _, group := range r.addresses {
		addressGroups = append(addressGroups, strings.Join(group, ","))
	}
	return strings.Join(addressGroups, ";")
}

// ParsePrioritizedRoundRobinRemote parses a string into a prioritized
// round robin Remote. See doc for NewPrioritizedRoundRobinRemote for details
// on the returned Remote.
//
// Example:
//
//	"example0.com,example1.com;example0.net,example1.net" produces a
//	prioritized round robin remote with two groups, first .com then .net.
func ParsePrioritizedRoundRobinRemote(str string) (Remote, error) {
	groups := strings.Split(str, ";")
	addressGroups := make([][]string, 0, len(groups))
	for _, group := range groups {
		addressGroups = append(addressGroups, strings.Split(group, ","))
	}
	return NewPrioritizedRoundRobinRemote(addressGroups)
}
<|endoftext|>


<commit_before>package githttp

import (
	"io"
	"regexp"
)

// RpcReader scans for events in the incoming rpc request data
type RpcReader struct {
	// Underlaying reader (to relay calls to)
	io.Reader

	// Rpc type (upload-pack or receive-pack)
	Rpc string

	// List of events RpcReader has picked up through scanning
	// these events do not contain the "Dir" attribute
	Events []Event
}

// Regexes to detect types of actions (fetch, push, etc ...)
var (
	receivePackRegex = regexp.MustCompile("([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) refs\\/(heads|tags)\\/(.*?)( |00|\u0000)|^(0000)$")
	uploadPackRegex  = regexp.MustCompile("^\\S+ ([0-9a-fA-F]{40})")
)

// Implement the io.Reader interface
func (r *RpcReader) Read(p []byte) (n int, err error) {
	// Relay call
	n, err = r.Reader.Read(p)

	// Scan for events
	if err == nil || err == io.EOF {
		r.scan(p)
	}

	return n, err
}

func (r *RpcReader) scan(p []byte) {
	events := []Event{}

	switch r.Rpc {
	case "receive-pack":
		events = scanPush(p)
	case "upload-pack":
		events = scanFetch(p)
	}

	// Add new events
	if len(events) > 0 {
		r.Events = append(r.Events, events...)
	}
}

func scanFetch(data []byte) []Event {
	matches := uploadPackRegex.FindAllStringSubmatch(string(data[:]), -1)

	if matches == nil {
		return []Event{}
	}

	events := []Event{}
	for _, m := range matches {
		events = append(events, Event{
			Type:   FETCH,
			Commit: m[1],
		})
	}

	return events
}

func scanPush(data []byte) []Event {
	matches := receivePackRegex.FindAllStringSubmatch(string(data[:]), -1)

	if matches == nil {
		return []Event{}
	}

	events := []Event{}
	for _, m := range matches {
		e := Event{
			Last:   m[1],
			Commit: m[2],
		}

		// Handle pushes to branches and tags differently
		if m[3] == "heads" {
			e.Type = PUSH
			e.Branch = m[4]
		} else {
			e.Type = TAG
			e.Tag = m[4]
		}

		events = append(events, e)
	}

	return events
}
<commit_msg>Fix incorrect io.Reader output usage.<commit_after>package githttp

import (
	"io"
	"regexp"
)

// RpcReader scans for events in the incoming rpc request data
type RpcReader struct {
	// Underlaying reader (to relay calls to)
	io.Reader

	// Rpc type (upload-pack or receive-pack)
	Rpc string

	// List of events RpcReader has picked up through scanning
	// these events do not contain the "Dir" attribute
	Events []Event
}

// Regexes to detect types of actions (fetch, push, etc ...)
var (
	receivePackRegex = regexp.MustCompile("([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) refs\\/(heads|tags)\\/(.*?)( |00|\u0000)|^(0000)$")
	uploadPackRegex  = regexp.MustCompile("^\\S+ ([0-9a-fA-F]{40})")
)

// Implement the io.Reader interface
func (r *RpcReader) Read(p []byte) (n int, err error) {
	// Relay call
	n, err = r.Reader.Read(p)

	// Scan for events
	if n > 0 {
		r.scan(p[:n])
	}

	return n, err
}

func (r *RpcReader) scan(p []byte) {
	events := []Event{}

	switch r.Rpc {
	case "receive-pack":
		events = scanPush(p)
	case "upload-pack":
		events = scanFetch(p)
	}

	// Add new events
	if len(events) > 0 {
		r.Events = append(r.Events, events...)
	}
}

func scanFetch(data []byte) []Event {
	matches := uploadPackRegex.FindAllStringSubmatch(string(data[:]), -1)

	if matches == nil {
		return []Event{}
	}

	events := []Event{}
	for _, m := range matches {
		events = append(events, Event{
			Type:   FETCH,
			Commit: m[1],
		})
	}

	return events
}

func scanPush(data []byte) []Event {
	matches := receivePackRegex.FindAllStringSubmatch(string(data[:]), -1)

	if matches == nil {
		return []Event{}
	}

	events := []Event{}
	for _, m := range matches {
		e := Event{
			Last:   m[1],
			Commit: m[2],
		}

		// Handle pushes to branches and tags differently
		if m[3] == "heads" {
			e.Type = PUSH
			e.Branch = m[4]
		} else {
			e.Type = TAG
			e.Tag = m[4]
		}

		events = append(events, e)
	}

	return events
}
<|endoftext|>


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

package snowstorm

import (
	"github.com/ava-labs/avalanchego/ids"
	"github.com/ava-labs/avalanchego/snow"
	"github.com/ava-labs/avalanchego/snow/choices"

	sbcon "github.com/ava-labs/avalanchego/snow/consensus/snowball"
)

// DirectedFactory implements Factory by returning a directed struct
type DirectedFactory struct{}

// New implements Factory
func (DirectedFactory) New() Consensus { return &Directed{} }

// Directed is an implementation of a multi-color, non-transitive, snowball
// instance
type Directed struct {
	common

	// Key: Transaction ID
	// Value: Node that represents this transaction in the conflict graph
	txs map[ids.ID]*directedTx

	// Key: UTXO ID
	// Value: IDs of transactions that consume the UTXO specified in the key
	utxos map[ids.ID]ids.Set
}

type directedTx struct {
	snowball

	// pendingAccept identifies if this transaction has been marked as accepted
	// once its transitive dependencies have also been accepted
	pendingAccept bool

	// ins is the set of txIDs that this tx conflicts with that are less
	// preferred than this tx
	ins ids.Set

	// outs is the set of txIDs that this tx conflicts with that are more
	// preferred than this tx
	outs ids.Set

	// tx is the actual transaction this node represents
	tx Tx
}

// Initialize implements the Consensus interface
func (dg *Directed) Initialize(
	ctx *snow.Context,
	params sbcon.Parameters,
) error {
	dg.txs = make(map[ids.ID]*directedTx)
	dg.utxos = make(map[ids.ID]ids.Set)

	return dg.common.Initialize(ctx, params)
}

// IsVirtuous implements the Consensus interface
func (dg *Directed) IsVirtuous(tx Tx) bool {
	txID := tx.ID()
	// If the tx is currently processing, we should just return if was
	// registered as rogue or not.
	if node, exists := dg.txs[txID]; exists {
		return !node.rogue
	}

	// The tx isn't processing, so we need to check to see if it conflicts with
	// any of the other txs that are currently processing.
	for _, utxoID := range tx.InputIDs() {
		if _, exists := dg.utxos[utxoID]; exists {
			// A currently processing tx names the same input as the provided
			// tx, so the provided tx would be rogue.
			return false
		}
	}

	// This tx is virtuous as far as this consensus instance knows.
	return true
}

// Conflicts implements the Consensus interface
func (dg *Directed) Conflicts(tx Tx) ids.Set {
	var conflicts ids.Set
	if node, exists := dg.txs[tx.ID()]; exists {
		// If the tx is currently processing, the conflicting txs are just the
		// union of the inbound conflicts and the outbound conflicts.
		// Only bother to call Union, which will do a memory allocation, if ins or outs are non-empty.
		if node.ins.Len() > 0 || node.outs.Len() > 0 {
			conflicts.Union(node.ins)
			conflicts.Union(node.outs)
		}
	} else {
		// If the tx isn't currently processing, the conflicting txs are the
		// union of all the txs that spend an input that this tx spends.
		for _, inputID := range tx.InputIDs() {
			if spends, exists := dg.utxos[inputID]; exists {
				conflicts.Union(spends)
			}
		}
	}
	return conflicts
}

// Add implements the Consensus interface
func (dg *Directed) Add(tx Tx) error {
	if shouldVote, err := dg.shouldVote(dg, tx); !shouldVote || err != nil {
		return err
	}

	txID := tx.ID()
	txNode := &directedTx{tx: tx}

	// For each UTXO consumed by the tx:
	// * Add edges between this tx and txs that consume this UTXO
	// * Mark this tx as attempting to consume this UTXO
	for _, inputID := range tx.InputIDs() {
		// Get the set of txs that are currently processing that also consume
		// this UTXO
		spenders := dg.utxos[inputID]

		// Add all the txs that spend this UTXO to this txs conflicts. These
		// conflicting txs must be preferred over this tx. We know this because
		// this tx currently has a bias of 0 and the tie goes to the tx whose
		// bias was updated first.
		txNode.outs.Union(spenders)

		// Update txs conflicting with tx to account for its issuance
		for conflictIDKey := range spenders {
			// Get the node that contains this conflicting tx
			conflict := dg.txs[conflictIDKey]

			// This conflicting tx can't be virtuous anymore. So, we attempt to
			// remove it from all of the virtuous sets.
			delete(dg.virtuous, conflictIDKey)
			delete(dg.virtuousVoting, conflictIDKey)

			// This tx should be set to rogue if it wasn't rogue before.
			conflict.rogue = true

			// This conflicting tx is preferred over the tx being inserted, as
			// described above. So we add the conflict to the inbound set.
			conflict.ins.Add(txID)
		}

		// Add this tx to list of txs consuming the current UTXO
		spenders.Add(txID)

		// Because this isn't a pointer, we should re-map the set.
		dg.utxos[inputID] = spenders
	}

	// Mark this transaction as rogue if had any conflicts registered above
	txNode.rogue = txNode.outs.Len() != 0

	if !txNode.rogue {
		// If this tx is currently virtuous, add it to the virtuous sets
		dg.virtuous.Add(txID)
		dg.virtuousVoting.Add(txID)

		// If a tx is virtuous, it must be preferred.
		dg.preferences.Add(txID)
	}

	// Add this tx to the set of currently processing txs
	dg.txs[txID] = txNode

	// If a tx that this tx depends on is rejected, this tx should also be
	// rejected.
	dg.registerRejector(dg, tx)
	return nil
}

// Issued implements the Consensus interface
func (dg *Directed) Issued(tx Tx) bool {
	// If the tx is either Accepted or Rejected, then it must have been issued
	// previously.
	if tx.Status().Decided() {
		return true
	}

	// If the tx is currently processing, then it must have been issued.
	_, ok := dg.txs[tx.ID()]
	return ok
}

// RecordPoll implements the Consensus interface
func (dg *Directed) RecordPoll(votes ids.Bag) (bool, error) {
	// Increase the vote ID. This is only updated here and is used to reset the
	// confidence values of transactions lazily.
	// This is also used to track the number of polls required to accept/reject
	// a transaction.
	dg.pollNumber++

	// This flag tracks if the Avalanche instance needs to recompute its
	// frontiers. Frontiers only need to be recalculated if preferences change
	// or if a tx was accepted.
	changed := false

	// We only want to iterate over txs that received alpha votes
	votes.SetThreshold(dg.params.Alpha)
	// Get the set of IDs that meet this alpha threshold
	metThreshold := votes.Threshold()
	for txIDKey := range metThreshold {
		// Get the node this tx represents
		txNode, exist := dg.txs[txIDKey]
		if !exist {
			// This tx may have already been accepted because of tx
			// dependencies. If this is the case, we can just drop the vote.
			continue
		}

		txNode.RecordSuccessfulPoll(dg.pollNumber)

		// If the tx should be accepted, then we should defer its acceptance
		// until its dependencies are decided. If this tx was already marked to
		// be accepted, we shouldn't register it again.
		if !txNode.pendingAccept &&
			txNode.Finalized(dg.params.BetaVirtuous, dg.params.BetaRogue) {
			// Mark that this tx is pending acceptance so acceptance is only
			// registered once.
			txNode.pendingAccept = true

			dg.registerAcceptor(dg, txNode.tx)
			if dg.errs.Errored() {
				return changed, dg.errs.Err
			}
		}

		if txNode.tx.Status() != choices.Accepted {
			// If this tx wasn't accepted, then this instance is only changed if
			// preferences changed.
			changed = dg.redirectEdges(txNode) || changed
		} else {
			// By accepting a tx, the state of this instance has changed.
			changed = true
		}
	}

	if len(dg.txs) > 0 {
		if metThreshold.Len() == 0 {
			dg.Failed()
		} else {
			dg.Successful()
		}
	}
	return changed, dg.errs.Err
}

func (dg *Directed) String() string {
	nodes := make([]*snowballNode, 0, len(dg.txs))
	for _, txNode := range dg.txs {
		nodes = append(nodes, &snowballNode{
			txID:               txNode.tx.ID(),
			numSuccessfulPolls: txNode.numSuccessfulPolls,
			confidence:         txNode.Confidence(dg.pollNumber),
		})
	}
	return ConsensusString("DG", nodes)
}

// accept the named txID and remove it from the graph
func (dg *Directed) accept(txID ids.ID) error {
	txNode := dg.txs[txID]
	// We are accepting the tx, so we should remove the node from the graph.
	delete(dg.txs, txID)

	// This tx is consuming all the UTXOs from its inputs, so we can prune them
	// all from memory
	for _, inputID := range txNode.tx.InputIDs() {
		delete(dg.utxos, inputID)
	}

	// This tx is now accepted, so it shouldn't be part of the virtuous set or
	// the preferred set. Its status as Accepted implies these descriptions.
	dg.virtuous.Remove(txID)
	dg.preferences.Remove(txID)

	// Reject all the txs that conflicted with this tx.
	if err := dg.reject(txNode.ins); err != nil {
		return err
	}
	// While it is typically true that a tx this is being accepted is preferred,
	// it is possible for this to not be the case. So this is handled for
	// completeness.
	if err := dg.reject(txNode.outs); err != nil {
		return err
	}
	return dg.acceptTx(txNode.tx)
}

// reject all the named txIDs and remove them from the graph
func (dg *Directed) reject(conflictIDs ids.Set) error {
	for conflictKey := range conflictIDs {
		conflict := dg.txs[conflictKey]
		// This tx is no longer an option for consuming the UTXOs from its
		// inputs, so we should remove their reference to this tx.
		for _, inputID := range conflict.tx.InputIDs() {
			txIDs, exists := dg.utxos[inputID]
			if !exists {
				// This UTXO may no longer exist because it was removed due to
				// the acceptance of a tx. If that is the case, there is nothing
				// left to remove from memory.
				continue
			}
			delete(txIDs, conflictKey)
			if txIDs.Len() == 0 {
				// If this tx was the last tx consuming this UTXO, we should
				// prune the UTXO from memory entirely.
				delete(dg.utxos, inputID)
			} else {
				// If this UTXO still has txs consuming it, then we should make
				// sure this update is written back to the UTXOs map.
				dg.utxos[inputID] = txIDs
			}
		}

		// We are rejecting the tx, so we should remove it from the graph
		delete(dg.txs, conflictKey)

		// While it's statistically unlikely that something being rejected is
		// preferred, it is handled for completion.
		delete(dg.preferences, conflictKey)

		// remove the edge between this node and all its neighbors
		dg.removeConflict(conflictKey, conflict.ins)
		dg.removeConflict(conflictKey, conflict.outs)

		if err := dg.rejectTx(conflict.tx); err != nil {
			return err
		}
	}
	return nil
}

// redirectEdges attempts to turn outbound edges into inbound edges if the
// preferences have changed
func (dg *Directed) redirectEdges(tx *directedTx) bool {
	changed := false
	for conflictID := range tx.outs {
		changed = dg.redirectEdge(tx, conflictID) || changed
	}
	return changed
}

// Change the direction of this edge if needed. Returns true if the direction
// was switched.
// TODO replace
func (dg *Directed) redirectEdge(txNode *directedTx, conflictID ids.ID) bool {
	conflict := dg.txs[conflictID]
	if txNode.numSuccessfulPolls <= conflict.numSuccessfulPolls {
		return false
	}

	// Because this tx has a higher preference than the conflicting tx, we must
	// ensure that the edge is directed towards this tx.
	nodeID := txNode.tx.ID()

	// Change the edge direction according to the conflict tx
	conflict.ins.Remove(nodeID)
	conflict.outs.Add(nodeID)
	dg.preferences.Remove(conflictID) // This conflict has an outbound edge

	// Change the edge direction according to this tx
	txNode.ins.Add(conflictID)
	txNode.outs.Remove(conflictID)
	if txNode.outs.Len() == 0 {
		// If this tx doesn't have any outbound edges, it's preferred
		dg.preferences.Add(nodeID)
	}
	return true
}

func (dg *Directed) removeConflict(txIDKey ids.ID, neighborIDs ids.Set) {
	for neighborID := range neighborIDs {
		neighbor, exists := dg.txs[neighborID]
		if !exists {
			// If the neighbor doesn't exist, they may have already been
			// rejected, so this mapping can be skipped.
			continue
		}

		// Remove any edge to this tx.
		delete(neighbor.ins, txIDKey)
		delete(neighbor.outs, txIDKey)

		if neighbor.outs.Len() == 0 {
			// If this tx should now be preferred, make sure its status is
			// updated.
			dg.preferences.Add(neighborID)
		}
	}
}
<commit_msg>rm useless<commit_after>// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package snowstorm

import (
	"github.com/ava-labs/avalanchego/ids"
	"github.com/ava-labs/avalanchego/snow"
	"github.com/ava-labs/avalanchego/snow/choices"

	sbcon "github.com/ava-labs/avalanchego/snow/consensus/snowball"
)

// DirectedFactory implements Factory by returning a directed struct
type DirectedFactory struct{}

// New implements Factory
func (DirectedFactory) New() Consensus { return &Directed{} }

// Directed is an implementation of a multi-color, non-transitive, snowball
// instance
type Directed struct {
	common

	// Key: Transaction ID
	// Value: Node that represents this transaction in the conflict graph
	txs map[ids.ID]*directedTx

	// Key: UTXO ID
	// Value: IDs of transactions that consume the UTXO specified in the key
	utxos map[ids.ID]ids.Set
}

type directedTx struct {
	snowball

	// pendingAccept identifies if this transaction has been marked as accepted
	// once its transitive dependencies have also been accepted
	pendingAccept bool

	// ins is the set of txIDs that this tx conflicts with that are less
	// preferred than this tx
	ins ids.Set

	// outs is the set of txIDs that this tx conflicts with that are more
	// preferred than this tx
	outs ids.Set

	// tx is the actual transaction this node represents
	tx Tx
}

// Initialize implements the Consensus interface
func (dg *Directed) Initialize(
	ctx *snow.Context,
	params sbcon.Parameters,
) error {
	dg.txs = make(map[ids.ID]*directedTx)
	dg.utxos = make(map[ids.ID]ids.Set)

	return dg.common.Initialize(ctx, params)
}

// IsVirtuous implements the Consensus interface
func (dg *Directed) IsVirtuous(tx Tx) bool {
	txID := tx.ID()
	// If the tx is currently processing, we should just return if was
	// registered as rogue or not.
	if node, exists := dg.txs[txID]; exists {
		return !node.rogue
	}

	// The tx isn't processing, so we need to check to see if it conflicts with
	// any of the other txs that are currently processing.
	for _, utxoID := range tx.InputIDs() {
		if _, exists := dg.utxos[utxoID]; exists {
			// A currently processing tx names the same input as the provided
			// tx, so the provided tx would be rogue.
			return false
		}
	}

	// This tx is virtuous as far as this consensus instance knows.
	return true
}

// Conflicts implements the Consensus interface
func (dg *Directed) Conflicts(tx Tx) ids.Set {
	var conflicts ids.Set
	if node, exists := dg.txs[tx.ID()]; exists {
		// If the tx is currently processing, the conflicting txs are just the
		// union of the inbound conflicts and the outbound conflicts.
		// Only bother to call Union, which will do a memory allocation, if ins or outs are non-empty.
		if node.ins.Len() > 0 || node.outs.Len() > 0 {
			conflicts.Union(node.ins)
			conflicts.Union(node.outs)
		}
	} else {
		// If the tx isn't currently processing, the conflicting txs are the
		// union of all the txs that spend an input that this tx spends.
		for _, inputID := range tx.InputIDs() {
			if spends, exists := dg.utxos[inputID]; exists {
				conflicts.Union(spends)
			}
		}
	}
	return conflicts
}

// Add implements the Consensus interface
func (dg *Directed) Add(tx Tx) error {
	if shouldVote, err := dg.shouldVote(dg, tx); !shouldVote || err != nil {
		return err
	}

	txID := tx.ID()
	txNode := &directedTx{tx: tx}

	// For each UTXO consumed by the tx:
	// * Add edges between this tx and txs that consume this UTXO
	// * Mark this tx as attempting to consume this UTXO
	for _, inputID := range tx.InputIDs() {
		// Get the set of txs that are currently processing that also consume
		// this UTXO
		spenders := dg.utxos[inputID]

		// Add all the txs that spend this UTXO to this txs conflicts. These
		// conflicting txs must be preferred over this tx. We know this because
		// this tx currently has a bias of 0 and the tie goes to the tx whose
		// bias was updated first.
		txNode.outs.Union(spenders)

		// Update txs conflicting with tx to account for its issuance
		for conflictIDKey := range spenders {
			// Get the node that contains this conflicting tx
			conflict := dg.txs[conflictIDKey]

			// This conflicting tx can't be virtuous anymore. So, we attempt to
			// remove it from all of the virtuous sets.
			delete(dg.virtuous, conflictIDKey)
			delete(dg.virtuousVoting, conflictIDKey)

			// This tx should be set to rogue if it wasn't rogue before.
			conflict.rogue = true

			// This conflicting tx is preferred over the tx being inserted, as
			// described above. So we add the conflict to the inbound set.
			conflict.ins.Add(txID)
		}

		// Add this tx to list of txs consuming the current UTXO
		spenders.Add(txID)

	}

	// Mark this transaction as rogue if had any conflicts registered above
	txNode.rogue = txNode.outs.Len() != 0

	if !txNode.rogue {
		// If this tx is currently virtuous, add it to the virtuous sets
		dg.virtuous.Add(txID)
		dg.virtuousVoting.Add(txID)

		// If a tx is virtuous, it must be preferred.
		dg.preferences.Add(txID)
	}

	// Add this tx to the set of currently processing txs
	dg.txs[txID] = txNode

	// If a tx that this tx depends on is rejected, this tx should also be
	// rejected.
	dg.registerRejector(dg, tx)
	return nil
}

// Issued implements the Consensus interface
func (dg *Directed) Issued(tx Tx) bool {
	// If the tx is either Accepted or Rejected, then it must have been issued
	// previously.
	if tx.Status().Decided() {
		return true
	}

	// If the tx is currently processing, then it must have been issued.
	_, ok := dg.txs[tx.ID()]
	return ok
}

// RecordPoll implements the Consensus interface
func (dg *Directed) RecordPoll(votes ids.Bag) (bool, error) {
	// Increase the vote ID. This is only updated here and is used to reset the
	// confidence values of transactions lazily.
	// This is also used to track the number of polls required to accept/reject
	// a transaction.
	dg.pollNumber++

	// This flag tracks if the Avalanche instance needs to recompute its
	// frontiers. Frontiers only need to be recalculated if preferences change
	// or if a tx was accepted.
	changed := false

	// We only want to iterate over txs that received alpha votes
	votes.SetThreshold(dg.params.Alpha)
	// Get the set of IDs that meet this alpha threshold
	metThreshold := votes.Threshold()
	for txIDKey := range metThreshold {
		// Get the node this tx represents
		txNode, exist := dg.txs[txIDKey]
		if !exist {
			// This tx may have already been accepted because of tx
			// dependencies. If this is the case, we can just drop the vote.
			continue
		}

		txNode.RecordSuccessfulPoll(dg.pollNumber)

		// If the tx should be accepted, then we should defer its acceptance
		// until its dependencies are decided. If this tx was already marked to
		// be accepted, we shouldn't register it again.
		if !txNode.pendingAccept &&
			txNode.Finalized(dg.params.BetaVirtuous, dg.params.BetaRogue) {
			// Mark that this tx is pending acceptance so acceptance is only
			// registered once.
			txNode.pendingAccept = true

			dg.registerAcceptor(dg, txNode.tx)
			if dg.errs.Errored() {
				return changed, dg.errs.Err
			}
		}

		if txNode.tx.Status() != choices.Accepted {
			// If this tx wasn't accepted, then this instance is only changed if
			// preferences changed.
			changed = dg.redirectEdges(txNode) || changed
		} else {
			// By accepting a tx, the state of this instance has changed.
			changed = true
		}
	}

	if len(dg.txs) > 0 {
		if metThreshold.Len() == 0 {
			dg.Failed()
		} else {
			dg.Successful()
		}
	}
	return changed, dg.errs.Err
}

func (dg *Directed) String() string {
	nodes := make([]*snowballNode, 0, len(dg.txs))
	for _, txNode := range dg.txs {
		nodes = append(nodes, &snowballNode{
			txID:               txNode.tx.ID(),
			numSuccessfulPolls: txNode.numSuccessfulPolls,
			confidence:         txNode.Confidence(dg.pollNumber),
		})
	}
	return ConsensusString("DG", nodes)
}

// accept the named txID and remove it from the graph
func (dg *Directed) accept(txID ids.ID) error {
	txNode := dg.txs[txID]
	// We are accepting the tx, so we should remove the node from the graph.
	delete(dg.txs, txID)

	// This tx is consuming all the UTXOs from its inputs, so we can prune them
	// all from memory
	for _, inputID := range txNode.tx.InputIDs() {
		delete(dg.utxos, inputID)
	}

	// This tx is now accepted, so it shouldn't be part of the virtuous set or
	// the preferred set. Its status as Accepted implies these descriptions.
	dg.virtuous.Remove(txID)
	dg.preferences.Remove(txID)

	// Reject all the txs that conflicted with this tx.
	if err := dg.reject(txNode.ins); err != nil {
		return err
	}
	// While it is typically true that a tx this is being accepted is preferred,
	// it is possible for this to not be the case. So this is handled for
	// completeness.
	if err := dg.reject(txNode.outs); err != nil {
		return err
	}
	return dg.acceptTx(txNode.tx)
}

// reject all the named txIDs and remove them from the graph
func (dg *Directed) reject(conflictIDs ids.Set) error {
	for conflictKey := range conflictIDs {
		conflict := dg.txs[conflictKey]
		// This tx is no longer an option for consuming the UTXOs from its
		// inputs, so we should remove their reference to this tx.
		for _, inputID := range conflict.tx.InputIDs() {
			txIDs, exists := dg.utxos[inputID]
			if !exists {
				// This UTXO may no longer exist because it was removed due to
				// the acceptance of a tx. If that is the case, there is nothing
				// left to remove from memory.
				continue
			}
			delete(txIDs, conflictKey)
			if txIDs.Len() == 0 {
				// If this tx was the last tx consuming this UTXO, we should
				// prune the UTXO from memory entirely.
				delete(dg.utxos, inputID)
			} else {
				// If this UTXO still has txs consuming it, then we should make
				// sure this update is written back to the UTXOs map.
				dg.utxos[inputID] = txIDs
			}
		}

		// We are rejecting the tx, so we should remove it from the graph
		delete(dg.txs, conflictKey)

		// While it's statistically unlikely that something being rejected is
		// preferred, it is handled for completion.
		delete(dg.preferences, conflictKey)

		// remove the edge between this node and all its neighbors
		dg.removeConflict(conflictKey, conflict.ins)
		dg.removeConflict(conflictKey, conflict.outs)

		if err := dg.rejectTx(conflict.tx); err != nil {
			return err
		}
	}
	return nil
}

// redirectEdges attempts to turn outbound edges into inbound edges if the
// preferences have changed
func (dg *Directed) redirectEdges(tx *directedTx) bool {
	changed := false
	for conflictID := range tx.outs {
		changed = dg.redirectEdge(tx, conflictID) || changed
	}
	return changed
}

// Change the direction of this edge if needed. Returns true if the direction
// was switched.
// TODO replace
func (dg *Directed) redirectEdge(txNode *directedTx, conflictID ids.ID) bool {
	conflict := dg.txs[conflictID]
	if txNode.numSuccessfulPolls <= conflict.numSuccessfulPolls {
		return false
	}

	// Because this tx has a higher preference than the conflicting tx, we must
	// ensure that the edge is directed towards this tx.
	nodeID := txNode.tx.ID()

	// Change the edge direction according to the conflict tx
	conflict.ins.Remove(nodeID)
	conflict.outs.Add(nodeID)
	dg.preferences.Remove(conflictID) // This conflict has an outbound edge

	// Change the edge direction according to this tx
	txNode.ins.Add(conflictID)
	txNode.outs.Remove(conflictID)
	if txNode.outs.Len() == 0 {
		// If this tx doesn't have any outbound edges, it's preferred
		dg.preferences.Add(nodeID)
	}
	return true
}

func (dg *Directed) removeConflict(txIDKey ids.ID, neighborIDs ids.Set) {
	for neighborID := range neighborIDs {
		neighbor, exists := dg.txs[neighborID]
		if !exists {
			// If the neighbor doesn't exist, they may have already been
			// rejected, so this mapping can be skipped.
			continue
		}

		// Remove any edge to this tx.
		delete(neighbor.ins, txIDKey)
		delete(neighbor.outs, txIDKey)

		if neighbor.outs.Len() == 0 {
			// If this tx should now be preferred, make sure its status is
			// updated.
			dg.preferences.Add(neighborID)
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2015 The LUCI Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"time"

	"github.com/luci/luci-go/common/clock/clockflag"
	"github.com/luci/luci-go/common/data/rand/mathrand"
	log "github.com/luci/luci-go/common/logging"
	"github.com/luci/luci-go/common/logging/gologger"
	"github.com/luci/luci-go/common/proto/milo"
	"github.com/luci/luci-go/common/runtime/profiling"
	"github.com/luci/luci-go/logdog/client/annotee"
	"github.com/luci/luci-go/logdog/client/annotee/executor"
	"github.com/luci/luci-go/logdog/client/bootstrapResult"
	"github.com/luci/luci-go/logdog/client/butlerlib/bootstrap"
	"github.com/luci/luci-go/logdog/client/butlerlib/streamclient"
	"github.com/luci/luci-go/logdog/client/butlerlib/streamproto"
	"github.com/luci/luci-go/logdog/common/types"
	"github.com/luci/luci-go/luci_config/common/cfgtypes"

	"github.com/golang/protobuf/proto"
	"golang.org/x/net/context"
)

const (
	// configErrorReturnCode is returned when there is an error with Annotee's
	// command-line configuration.
	configErrorReturnCode = 2

	// runtimeErrorReturnCode is returned when the execution fails due to an
	// Annotee runtime error. This is intended to help differentiate Annotee
	// errors from passthrough bootstrapped subprocess errors.
	//
	// This will only be returned for runtime errors. If there is a flag error
	// or a configuration error, standard Annotee return codes (likely to overlap
	// with standard process return codes) will be used.
	//
	// This value has been chosen so as not to conflict with LogDog Butler runtime
	// error return code, allowing users to differentiate between Butler and
	// Annotee errors.
	runtimeErrorReturnCode = 251

	// defaultAnnotationInterval is the default annotation interval value.
	defaultAnnotationInterval = 30 * time.Second
)

type application struct {
	context.Context

	resultPath         string
	jsonArgsPath       string
	butlerStreamServer string
	tee                teeFlag
	printSummary       bool
	testingDir         string
	annotationInterval clockflag.Duration
	project            cfgtypes.ProjectName
	nameBase           streamproto.StreamNameFlag
	prefix             streamproto.StreamNameFlag
	logdogHost         string

	prof profiling.Profiler

	bootstrap *bootstrap.Bootstrap
}

func (a *application) addToFlagSet(fs *flag.FlagSet) {
	fs.StringVar(&a.resultPath, "result-path", "",
		"If supplied, a JSON file describing the bootstrap result will be written here if the bootstrapped process "+
			"is successfully executed.")
	fs.StringVar(&a.jsonArgsPath, "json-args-path", "",
		"If specified, this is a JSON file containing the full command to run as an "+
			"array of strings.")
	fs.StringVar(&a.butlerStreamServer, "butler-stream-server", "",
		"The Butler stream server location. If empty, Annotee will check for Butler "+
			"bootstrapping and extract the stream server from that.")
	fs.Var(&a.tee, "tee",
		"Comma-delimited list of content to tee to the bootstrapped process. Valid values are: "+teeFlagOptions)
	fs.BoolVar(&a.printSummary, "print-summary", true,
		"Print the annotation protobufs that were emitted at the end.")
	fs.StringVar(&a.testingDir, "testing-dir", "",
		"Rather than coupling to a Butler instance, output generated annotations "+
			"and streams to this directory.")
	fs.Var(&a.annotationInterval, "annotation-interval",
		"Buffer annotation updates for this amount of time. <=0 sends every update.")
	fs.Var(&a.project, "project", "The log prefix's project name (required).")
	fs.Var(&a.nameBase, "name-base", "Base stream name to prepend to generated names.")
	fs.Var(&a.prefix, "prefix", "The log stream prefix. If missing, one will be inferred from bootstrap.")
	fs.StringVar(&a.logdogHost, "logdog-host", "",
		"LogDog Coordinator host name. If supplied, log viewing links will be generated.")
}

func (a *application) loadJSONArgs() ([]string, error) {
	fd, err := os.Open(a.jsonArgsPath)
	if err != nil {
		return nil, err
	}
	defer fd.Close()

	dec := json.NewDecoder(fd)
	args := []string(nil)
	if err := dec.Decode(&args); err != nil {
		return nil, err
	}
	return args, nil
}

func (a *application) getStreamClient() (streamclient.Client, error) {
	if a.testingDir != "" {
		return newFilesystemClient(a.testingDir)
	}

	if a.butlerStreamServer != "" {
		return streamclient.New(a.butlerStreamServer)
	}

	// Check our bootstrap.
	if a.bootstrap != nil && a.bootstrap.Client != nil {
		return a.bootstrap.Client, nil
	}

	return nil, errors.New("unable to identify stream client")
}

func (a *application) maybeWriteResult(r *bootstrapResult.Result) error {
	if a.resultPath == "" {
		return nil
	}

	log.Fields{
		"path": a.resultPath,
	}.Debugf(a, "Writing bootstrap result.")
	return r.WriteJSON(a.resultPath)
}

func mainImpl(args []string) int {
	ctx := gologger.StdConfig.Use(context.Background())

	logFlags := log.Config{
		Level: log.Warning,
	}
	a := application{
		Context:            ctx,
		annotationInterval: clockflag.Duration(defaultAnnotationInterval),
	}

	// Determine bootstrapped process arguments.
	var err error
	a.bootstrap, err = bootstrap.Get()
	if err != nil {
		log.WithError(err).Warningf(a, "Could not get LogDog Butler bootstrap information.")
	}

	fs := &flag.FlagSet{}
	logFlags.AddFlags(fs)
	a.prof.AddFlags(fs)
	a.addToFlagSet(fs)
	if err := fs.Parse(args); err != nil {
		log.WithError(err).Errorf(a, "Failed to parse flags.")
		return configErrorReturnCode
	}
	a.Context = logFlags.Set(a.Context)

	client, err := a.getStreamClient()
	if err != nil {
		log.WithError(err).Errorf(a, "Failed to get stream client instance.")
		return configErrorReturnCode
	}

	prefix := types.StreamName(a.prefix)
	if prefix == "" && a.bootstrap != nil {
		prefix = a.bootstrap.Prefix
	}

	// Get the annotation project. This must be non-empty.
	if a.project == "" && a.bootstrap != nil {
		a.project = a.bootstrap.Project
	}
	if err := a.project.Validate(); err != nil {
		log.WithError(err).Errorf(a, "Invalid project (-project).")
		return configErrorReturnCode
	}

	args = fs.Args()
	if a.jsonArgsPath != "" {
		if len(args) > 0 {
			log.Fields{
				"commandLineArgs": args,
				"jsonArgsPath":    a.jsonArgsPath,
			}.Errorf(a, "Cannot specify both JSON and command-line arguments.")
			return configErrorReturnCode
		}

		args, err = a.loadJSONArgs()
		if err != nil {
			log.Fields{
				log.ErrorKey:   err,
				"jsonArgsPath": a.jsonArgsPath,
			}.Errorf(a, "Failed to load JSON arguments.")
			return configErrorReturnCode
		}
	}
	if len(args) == 0 {
		log.Errorf(a, "No command-line arguments were supplied.")
		return configErrorReturnCode
	}

	// Translate "<=0" flag option to Processor's "0", indicating that every
	// update should be sent.
	if a.annotationInterval < 0 {
		a.annotationInterval = 0
	}

	// Start our profiling service. This will be a no-op if the profiler is not
	// configured.
	a.prof.Logger = log.Get(a)
	if err := a.prof.Start(); err != nil {
		log.WithError(err).Errorf(a, "Failed to start profiler.")
		return runtimeErrorReturnCode
	}

	// Initialize our link generator, if we can.
	e := executor.Executor{
		Options: annotee.Options{
			Base:                   types.StreamName(a.nameBase),
			Client:                 client,
			MetadataUpdateInterval: time.Duration(a.annotationInterval),
			CloseSteps:             true,
			TeeAnnotations:         a.tee.annotations,
			TeeText:                a.tee.text,
		},

		Stdin: os.Stdin,
	}

	linkGen := &coordinatorLinkGenerator{
		host:    a.logdogHost,
		project: a.project,
		prefix:  prefix,
	}
	if linkGen.canGenerateLinks() {
		e.Options.LinkGenerator = linkGen
	}

	if a.tee.enabled() {
		e.TeeStdout = os.Stdout
		e.TeeStderr = os.Stderr
	}
	if err := e.Run(a, args); err != nil {
		log.Fields{
			log.ErrorKey: err,
			"returnCode": e.ReturnCode(),
		}.Errorf(a, "Failed during execution.")
	}

	// Display a summary!
	if a.printSummary {
		// Unmarshal our Step data.
		var st milo.Step
		if err := proto.Unmarshal(e.Step(), &st); err == nil {
			fmt.Printf("=== Annotee: %q ===\n", st.Name)
			fmt.Println(proto.MarshalTextString(&st))
		} else {
			log.WithError(err).Warningf(a, "Failed to unmarshal end step data. Cannot show summary.")
		}
	}

	if !e.Executed() {
		return runtimeErrorReturnCode
	}

	br := bootstrapResult.Result{
		ReturnCode: e.ReturnCode(),
		Command:    args,
	}
	if err := a.maybeWriteResult(&br); err != nil {
		log.WithError(err).Warningf(a, "Failed to write bootstrap result.")
	}
	return e.ReturnCode()
}

func main() {
	mathrand.SeedRandomly()
	os.Exit(mainImpl(os.Args[1:]))
}
<commit_msg>Annotee: Automatically infer host from bootstrap.<commit_after>// Copyright 2015 The LUCI Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"time"

	"github.com/luci/luci-go/common/clock/clockflag"
	"github.com/luci/luci-go/common/data/rand/mathrand"
	log "github.com/luci/luci-go/common/logging"
	"github.com/luci/luci-go/common/logging/gologger"
	"github.com/luci/luci-go/common/proto/milo"
	"github.com/luci/luci-go/common/runtime/profiling"
	"github.com/luci/luci-go/logdog/client/annotee"
	"github.com/luci/luci-go/logdog/client/annotee/executor"
	"github.com/luci/luci-go/logdog/client/bootstrapResult"
	"github.com/luci/luci-go/logdog/client/butlerlib/bootstrap"
	"github.com/luci/luci-go/logdog/client/butlerlib/streamclient"
	"github.com/luci/luci-go/logdog/client/butlerlib/streamproto"
	"github.com/luci/luci-go/logdog/common/types"
	"github.com/luci/luci-go/luci_config/common/cfgtypes"

	"github.com/golang/protobuf/proto"
	"golang.org/x/net/context"
)

const (
	// configErrorReturnCode is returned when there is an error with Annotee's
	// command-line configuration.
	configErrorReturnCode = 2

	// runtimeErrorReturnCode is returned when the execution fails due to an
	// Annotee runtime error. This is intended to help differentiate Annotee
	// errors from passthrough bootstrapped subprocess errors.
	//
	// This will only be returned for runtime errors. If there is a flag error
	// or a configuration error, standard Annotee return codes (likely to overlap
	// with standard process return codes) will be used.
	//
	// This value has been chosen so as not to conflict with LogDog Butler runtime
	// error return code, allowing users to differentiate between Butler and
	// Annotee errors.
	runtimeErrorReturnCode = 251

	// defaultAnnotationInterval is the default annotation interval value.
	defaultAnnotationInterval = 30 * time.Second
)

type application struct {
	context.Context

	resultPath         string
	jsonArgsPath       string
	butlerStreamServer string
	tee                teeFlag
	printSummary       bool
	testingDir         string
	annotationInterval clockflag.Duration
	project            cfgtypes.ProjectName
	nameBase           streamproto.StreamNameFlag
	prefix             streamproto.StreamNameFlag
	logdogHost         string

	prof profiling.Profiler

	bootstrap *bootstrap.Bootstrap
}

func (a *application) addToFlagSet(fs *flag.FlagSet) {
	fs.StringVar(&a.resultPath, "result-path", "",
		"If supplied, a JSON file describing the bootstrap result will be written here if the bootstrapped process "+
			"is successfully executed.")
	fs.StringVar(&a.jsonArgsPath, "json-args-path", "",
		"If specified, this is a JSON file containing the full command to run as an "+
			"array of strings.")
	fs.StringVar(&a.butlerStreamServer, "butler-stream-server", "",
		"The Butler stream server location. If empty, Annotee will check for Butler "+
			"bootstrapping and extract the stream server from that.")
	fs.Var(&a.tee, "tee",
		"Comma-delimited list of content to tee to the bootstrapped process. Valid values are: "+teeFlagOptions)
	fs.BoolVar(&a.printSummary, "print-summary", true,
		"Print the annotation protobufs that were emitted at the end.")
	fs.StringVar(&a.testingDir, "testing-dir", "",
		"Rather than coupling to a Butler instance, output generated annotations "+
			"and streams to this directory.")
	fs.Var(&a.annotationInterval, "annotation-interval",
		"Buffer annotation updates for this amount of time. <=0 sends every update.")
	fs.Var(&a.project, "project", "The log prefix's project name (required).")
	fs.Var(&a.nameBase, "name-base", "Base stream name to prepend to generated names.")
	fs.Var(&a.prefix, "prefix", "The log stream prefix. If missing, one will be inferred from bootstrap.")
	fs.StringVar(&a.logdogHost, "logdog-host", "",
		"LogDog Coordinator host name. If supplied, log viewing links will be generated.")
}

func (a *application) loadJSONArgs() ([]string, error) {
	fd, err := os.Open(a.jsonArgsPath)
	if err != nil {
		return nil, err
	}
	defer fd.Close()

	dec := json.NewDecoder(fd)
	args := []string(nil)
	if err := dec.Decode(&args); err != nil {
		return nil, err
	}
	return args, nil
}

func (a *application) getStreamClient() (streamclient.Client, error) {
	if a.testingDir != "" {
		return newFilesystemClient(a.testingDir)
	}

	if a.butlerStreamServer != "" {
		return streamclient.New(a.butlerStreamServer)
	}

	// Check our bootstrap.
	if a.bootstrap != nil && a.bootstrap.Client != nil {
		return a.bootstrap.Client, nil
	}

	return nil, errors.New("unable to identify stream client")
}

func (a *application) maybeWriteResult(r *bootstrapResult.Result) error {
	if a.resultPath == "" {
		return nil
	}

	log.Fields{
		"path": a.resultPath,
	}.Debugf(a, "Writing bootstrap result.")
	return r.WriteJSON(a.resultPath)
}

func mainImpl(args []string) int {
	ctx := gologger.StdConfig.Use(context.Background())

	logFlags := log.Config{
		Level: log.Warning,
	}
	a := application{
		Context:            ctx,
		annotationInterval: clockflag.Duration(defaultAnnotationInterval),
	}

	// Determine bootstrapped process arguments.
	var err error
	a.bootstrap, err = bootstrap.Get()
	if err != nil {
		log.WithError(err).Warningf(a, "Could not get LogDog Butler bootstrap information.")
	}

	fs := &flag.FlagSet{}
	logFlags.AddFlags(fs)
	a.prof.AddFlags(fs)
	a.addToFlagSet(fs)
	if err := fs.Parse(args); err != nil {
		log.WithError(err).Errorf(a, "Failed to parse flags.")
		return configErrorReturnCode
	}
	a.Context = logFlags.Set(a.Context)

	client, err := a.getStreamClient()
	if err != nil {
		log.WithError(err).Errorf(a, "Failed to get stream client instance.")
		return configErrorReturnCode
	}

	prefix := types.StreamName(a.prefix)
	if prefix == "" && a.bootstrap != nil {
		prefix = a.bootstrap.Prefix
	}

	// Get the annotation project. This must be non-empty.
	if a.project == "" && a.bootstrap != nil {
		a.project = a.bootstrap.Project
	}
	if err := a.project.Validate(); err != nil {
		log.WithError(err).Errorf(a, "Invalid project (-project).")
		return configErrorReturnCode
	}

	if a.logdogHost == "" && a.bootstrap != nil {
		a.logdogHost = a.bootstrap.CoordinatorHost
	}

	args = fs.Args()
	if a.jsonArgsPath != "" {
		if len(args) > 0 {
			log.Fields{
				"commandLineArgs": args,
				"jsonArgsPath":    a.jsonArgsPath,
			}.Errorf(a, "Cannot specify both JSON and command-line arguments.")
			return configErrorReturnCode
		}

		args, err = a.loadJSONArgs()
		if err != nil {
			log.Fields{
				log.ErrorKey:   err,
				"jsonArgsPath": a.jsonArgsPath,
			}.Errorf(a, "Failed to load JSON arguments.")
			return configErrorReturnCode
		}
	}
	if len(args) == 0 {
		log.Errorf(a, "No command-line arguments were supplied.")
		return configErrorReturnCode
	}

	// Translate "<=0" flag option to Processor's "0", indicating that every
	// update should be sent.
	if a.annotationInterval < 0 {
		a.annotationInterval = 0
	}

	// Start our profiling service. This will be a no-op if the profiler is not
	// configured.
	a.prof.Logger = log.Get(a)
	if err := a.prof.Start(); err != nil {
		log.WithError(err).Errorf(a, "Failed to start profiler.")
		return runtimeErrorReturnCode
	}

	// Initialize our link generator, if we can.
	e := executor.Executor{
		Options: annotee.Options{
			Base:                   types.StreamName(a.nameBase),
			Client:                 client,
			MetadataUpdateInterval: time.Duration(a.annotationInterval),
			CloseSteps:             true,
			TeeAnnotations:         a.tee.annotations,
			TeeText:                a.tee.text,
		},

		Stdin: os.Stdin,
	}

	linkGen := &coordinatorLinkGenerator{
		host:    a.logdogHost,
		project: a.project,
		prefix:  prefix,
	}
	if linkGen.canGenerateLinks() {
		e.Options.LinkGenerator = linkGen
	}

	if a.tee.enabled() {
		e.TeeStdout = os.Stdout
		e.TeeStderr = os.Stderr
	}
	if err := e.Run(a, args); err != nil {
		log.Fields{
			log.ErrorKey: err,
			"returnCode": e.ReturnCode(),
		}.Errorf(a, "Failed during execution.")
	}

	// Display a summary!
	if a.printSummary {
		// Unmarshal our Step data.
		var st milo.Step
		if err := proto.Unmarshal(e.Step(), &st); err == nil {
			fmt.Printf("=== Annotee: %q ===\n", st.Name)
			fmt.Println(proto.MarshalTextString(&st))
		} else {
			log.WithError(err).Warningf(a, "Failed to unmarshal end step data. Cannot show summary.")
		}
	}

	if !e.Executed() {
		return runtimeErrorReturnCode
	}

	br := bootstrapResult.Result{
		ReturnCode: e.ReturnCode(),
		Command:    args,
	}
	if err := a.maybeWriteResult(&br); err != nil {
		log.WithError(err).Warningf(a, "Failed to write bootstrap result.")
	}
	return e.ReturnCode()
}

func main() {
	mathrand.SeedRandomly()
	os.Exit(mainImpl(os.Args[1:]))
}
<|endoftext|>


<commit_before>// clock
package main

import (
	"math"
	"os"
	"os/signal"
	"time"

	"github.com/ajstarks/openvg"
)

const (
	radians = math.Pi / 180
)

var hourangles = [12]float64{
	90, 60, 30, // 12, 1, 2
	0, 330, 300, // 3, 4, 5
	270, 240, 210, // 6, 7, 8
	180, 150, 120, // 9, 10, 11
}

var minangles = [60]float64{
	90, 84, 78, 72, 66, // 0-4
	60, 54, 48, 42, 36, // 5-9
	30, 24, 18, 12, 6, // 10-14
	0, 354, 348, 342, 336, // 15-19
	330, 324, 318, 312, 306, // 20-24
	300, 294, 288, 282, 276, // 25-29
	270, 264, 258, 252, 246, // 30-34
	240, 234, 228, 222, 216, // 35-39
	210, 204, 198, 192, 186, // 40-44
	180, 174, 168, 162, 156, // 45-49
	150, 144, 138, 132, 126, // 50-54
	120, 114, 108, 102, 96, // 55-59
}

var mindigits = [60]string{
	"00", "01", "02", "03", "04", "05",
	"06", "07", "08", "09", "10", "11",
	"12", "13", "14", "15", "16", "17",
	"18", "19", "20", "21", "22", "23",
	"24", "25", "26", "27", "28", "29",
	"30", "31", "32", "33", "34", "35",
	"36", "37", "38", "39", "40", "41",
	"42", "43", "44", "45", "46", "47",
	"48", "49", "50", "51", "52", "53",
	"54", "55", "56", "57", "58", "59",
}

func timecoord(x, y, r openvg.VGfloat, hour, min, sec int) (hx, hy, mx, my, sx, sy openvg.VGfloat) {
	rad := float64(r)
	mrad := rad * 0.9 // minute hand is 90% to the edge of the face
	srad := rad * 1.2 // second hand is outside the edge
	hrad := rad * 0.6 // hour hand is 60% to the edge

	// if the hour is > half-elapsed, adjust the hour angle to
	// reflect the fraction between the current and subsequent hour
	a := hourangles[hour%12]
	if min > 30 {
		a = a - (30.0 * (float64(min) / 60)) // deflection difference is 30 degrees
	}
	t := a * radians
	hx = x + openvg.VGfloat(hrad*math.Cos(t))
	hy = y + openvg.VGfloat(hrad*math.Sin(t))

	t = minangles[min] * radians
	mx = x + openvg.VGfloat(mrad*math.Cos(t))
	my = y + openvg.VGfloat(mrad*math.Sin(t))

	t = minangles[sec] * radians
	sx = x + openvg.VGfloat(srad*math.Cos(t))
	sy = y + openvg.VGfloat(srad*math.Sin(t))
	return
}

var hourdigits = []string{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"}

func face(x, y, r openvg.VGfloat, ts int) {
	var fx, fy, va openvg.VGfloat
	va = openvg.VGfloat(ts) / 2.0
	rad := float64(r)
	openvg.FillColor("black")
	for h := 12; h > 0; h-- {
		t := hourangles[h%12] * radians
		fx = x + openvg.VGfloat(rad*math.Cos(t))
		fy = y + openvg.VGfloat(rad*math.Sin(t))
		openvg.TextMid(fx, fy-va, hourdigits[h%12], "sans", ts)
	}
}

func main() {
	width, height := openvg.Init()
	cx := openvg.VGfloat(width / 2)
	cy := openvg.VGfloat(height / 2)
	facesize := openvg.VGfloat(cy * 0.5)
	textsize := facesize / 10.0
	framesize := facesize * 2.5
	hourstroke := textsize * 0.75
	minstroke := hourstroke / 2
	secsize := int(textsize * 0.4)

	// set up the ticker and signal handler
	ticker := time.NewTicker(1 * time.Second)
	sigint := make(chan os.Signal, 1)
	signal.Notify(sigint, os.Interrupt)

	// main loop: for each second, draw the clock components
	openvg.Start(width, height)
	for {
		select {
		case <-ticker.C:
			// get the current time
			now := time.Now()
			hr, min, sec := now.Clock()

			// compute element coordinates
			hx, hy, mx, my, sx, sy := timecoord(cx, cy, facesize, hr, min, sec)

			// reset each frame
			openvg.BackgroundColor("black")

			// frame and clock face
			openvg.FillRGB(127, 127, 127, 1)
			openvg.Roundrect(cx-framesize/2, cy-framesize/2, framesize, framesize, textsize, textsize)
			openvg.FillRGB(255, 255, 255, 1)
			openvg.Ellipse(cx, cy, facesize*2.2, facesize*2.2)
			face(cx, cy, facesize, int(textsize))

			// hour hand
			openvg.StrokeWidth(hourstroke)
			openvg.StrokeRGB(127, 127, 127, 1)
			openvg.Line(cx, cy, hx, hy)
			openvg.StrokeWidth(0)
			openvg.FillRGB(127, 127, 127, 1)
			openvg.Ellipse(hx, hy, hourstroke, hourstroke)

			// minute hand
			openvg.StrokeWidth(minstroke)
			openvg.StrokeRGB(127, 0, 0, 1)
			openvg.Line(cx, cy, mx, my)
			openvg.StrokeWidth(0)
			openvg.FillRGB(127, 0, 0, 1)
			openvg.Ellipse(mx, my, minstroke, minstroke)

			// center dot
			openvg.FillColor("black")
			openvg.Ellipse(cx, cy, textsize, textsize)

			// second indicator
			openvg.FillRGB(255, 255, 255, 1)
			openvg.TextMid(sx, sy, mindigits[sec], "sans", secsize)
			openvg.End()
		case <-sigint:
			openvg.Finish()
			return
		}
	}
}
<commit_msg>clock: simplify second display, remove unndeeded background<commit_after>// clock
package main

import (
	"math"
	"os"
	"os/signal"
	"time"

	"github.com/ajstarks/openvg"
)

const (
	radians = math.Pi / 180
)

var hourangles = [12]float64{
	90, 60, 30, // 12, 1, 2
	0, 330, 300, // 3, 4, 5
	270, 240, 210, // 6, 7, 8
	180, 150, 120, // 9, 10, 11
}

var minangles = [60]float64{
	90, 84, 78, 72, 66, // 0-4
	60, 54, 48, 42, 36, // 5-9
	30, 24, 18, 12, 6, // 10-14
	0, 354, 348, 342, 336, // 15-19
	330, 324, 318, 312, 306, // 20-24
	300, 294, 288, 282, 276, // 25-29
	270, 264, 258, 252, 246, // 30-34
	240, 234, 228, 222, 216, // 35-39
	210, 204, 198, 192, 186, // 40-44
	180, 174, 168, 162, 156, // 45-49
	150, 144, 138, 132, 126, // 50-54
	120, 114, 108, 102, 96, // 55-59
}

func timecoord(x, y, r openvg.VGfloat, hour, min, sec int) (hx, hy, mx, my, sx, sy openvg.VGfloat) {
	rad := float64(r)
	hrad := rad * 0.6 // hour hand is 60% to the edge of the face
	mrad := rad * 0.9 // minute hand is 90% to the edge
	srad := rad       // second hand is at the edge

	// if the hour is > half-elapsed, adjust the hour angle to
	// reflect the fraction between the current and subsequent hour
	t := hourangles[hour%12]
	if min > 30 {
		t = t - (30.0 * (float64(min) / 60)) // deflection difference is 30 degrees
	}
	t = t * radians
	hx = x + openvg.VGfloat(hrad*math.Cos(t))
	hy = y + openvg.VGfloat(hrad*math.Sin(t))

	t = minangles[min] * radians
	mx = x + openvg.VGfloat(mrad*math.Cos(t))
	my = y + openvg.VGfloat(mrad*math.Sin(t))

	t = minangles[sec] * radians
	sx = x + openvg.VGfloat(srad*math.Cos(t))
	sy = y + openvg.VGfloat(srad*math.Sin(t))
	return
}

var hourdigits = [12]string{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"}

func face(x, y, r openvg.VGfloat, ts int) {
	var fx, fy, va openvg.VGfloat
	va = openvg.VGfloat(ts) / 2.0
	rad := float64(r)
	openvg.FillColor("black")
	for h := 12; h > 0; h-- {
		t := hourangles[h%12] * radians
		fx = x + openvg.VGfloat(rad*math.Cos(t))
		fy = y + openvg.VGfloat(rad*math.Sin(t))
		openvg.TextMid(fx, fy-va, hourdigits[h%12], "sans", ts)
	}
}

func main() {
	width, height := openvg.Init()
	cx := openvg.VGfloat(width / 2)
	cy := openvg.VGfloat(height / 2)
	facesize := openvg.VGfloat(cy * 0.5)
	textsize := facesize / 10.0
	framesize := facesize * 2.5
	hourstroke := textsize
	minstroke := hourstroke * .6

	// set up the ticker and signal handler
	ticker := time.NewTicker(1 * time.Second)
	sigint := make(chan os.Signal, 1)
	signal.Notify(sigint, os.Interrupt)

	// main loop: for each second, draw the clock components
	openvg.Start(width, height)
	for {
		select {
		case <-ticker.C:
			// get the current time
			hr, min, sec := time.Now().Clock()

			// compute element coordinates
			hx, hy, mx, my, sx, sy := timecoord(cx, cy, facesize, hr, min, sec)

			// frame and clock face
			openvg.FillRGB(127, 127, 127, 1)
			openvg.Roundrect(cx-framesize/2, cy-framesize/2, framesize, framesize, textsize, textsize)
			openvg.FillRGB(255, 255, 255, 1)
			openvg.Ellipse(cx, cy, facesize*2.2, facesize*2.2)
			face(cx, cy, facesize, int(textsize))

			// hour hand
			openvg.StrokeWidth(hourstroke)
			openvg.StrokeRGB(127, 127, 127, 1)
			openvg.Line(cx, cy, hx, hy)
			openvg.StrokeWidth(0)
			openvg.FillRGB(127, 127, 127, 1)
			openvg.Ellipse(hx, hy, hourstroke, hourstroke)

			// minute hand
			openvg.StrokeWidth(minstroke)
			openvg.StrokeRGB(127, 0, 0, 1)
			openvg.Line(cx, cy, mx, my)
			openvg.StrokeWidth(0)
			openvg.FillRGB(127, 0, 0, 1)
			openvg.Ellipse(mx, my, minstroke, minstroke)

			// second indicator
			openvg.FillRGB(0, 0, 255, 0.3)
			openvg.Ellipse(sx, sy, textsize, textsize)

			// center dot
			openvg.FillColor("black")
			openvg.Ellipse(cx, cy, textsize, textsize)
			openvg.End()
		case <-sigint:
			openvg.Finish()
			return
		}
	}
}
<|endoftext|>