<commit_before>package mesos

import (
	"encoding/json"
	"net/http"
	"net/url"
)

type State struct {
	ActivatedSlaves int64   `json:"activated_slaves"`
	BuildDate       string  `json:"build_date"`
	BuildTime       float64 `json:"build_time"`
	BuildUser       string  `json:"build_user"`
	// CompletedFrameworks    []Something       `json:"completed_frameworks"`
	DeactivatedSlaves int64             `json:"deactivated_slaves"`
	ElectedTime       float64           `json:"elected_time"`
	FailedTasks       int64             `json:"failed_tasks"`
	FinishedTasks     int64             `json:"finished_tasks"`
	Flags             map[string]string `json:"flags"`
	Frameworks        []Framework       `json:"frameworks"`
	GitSHA            string            `json:"git_sha"`
	GitTag            string            `json:"git_tag"`
	Hostname          string            `json:"hostname"`
	ID                string            `json:"id"`
	KilledTasks       int64             `json:"killed_tasks"`
	Leader            string            `json:"leader"`
	LogDir            string            `json:"log_dir"`
	LostTasks         int64             `json:"lost_tasks"`
	// OrphanTasks            []Something       `json:"orphan_tasks"`
	PID          string  `json:"pid"`
	Slaves       []Slave `json:"slaves"`
	StagedTasks  int64   `json:"staged_tasks"`
	StartTime    float64 `json:"start_time"`
	StartedTasks int64   `json:"started_tasks"`
	// UnregisteredFrameworks []Something       `json:"unregistered_frameworks"`
	Version string `json:"version"`
}

type Slave struct {
	Active         bool              `json:"active"`
	Attributes     map[string]string `json:"attributes"`
	Hostname       string            `json:"hostname"`
	ID             string            `json:"id"`
	PID            string            `json:"pid"`
	RegisteredTime float64           `json:"registered_time"`
	Resources      Resources         `json:"resources"`
}

type Resources struct {
	CPUs  float64 `json:"cpus"`
	Disk  int64   `json:"disk"`
	Mem   int64   `json:"mem"`
	Ports string  `json:"ports"`
}

type Framework struct {
	Active           bool        `json:"active"`
	Checkpoint       bool        `json:"checkpoint"`
	CompletedTasks   []Task      `json:"checkpoint"`
	FailoverTimeout  int64       `json:"failover_timeout"`
	Hostname         string      `json:"hostname"`
	ID               string      `json:"id"`
	Name             string      `json:"name"`
	OfferedResources Resources   `json:"offered_resources"`
	Offers           []Resources `json:"offers"`
	RegisteredTime   float64     `json:"registered_time"`
	ReregisteredTime float64     `json:"reregistered_time"`
	Resources        Resources   `json:"resources"`
	Role             string      `json:"role"`
	Tasks            []Task      `json:"tasks"`
	UnregisteredTime float64     `json:"unregistered_time"`
	UsedResources    Resources   `json:"used_resources"`
	User             string      `json:"user"`
	WebUIURL         string      `json:"webui_url"`
}

type Task struct {
	ExecutorID  string    `json:"executor_id"`
	FrameworkID string    `json:"framework_id"`
	ID          string    `json:"id"`
	Labels      []string  `json:"labels"`
	Name        string    `json:"name"`
	Resources   Resources `json:"resources"`
	SlaveID     string    `json:"slave_id"`
	State       string    `json:"state"`
	Statuses    []Status  `json:"statuses"`
}

type Status struct {
	State     string  `json:"state"`
	Timestamp float64 `json:"timestamp"`
}

type Client struct {
	client *http.Client
	host   string
}

func NewClient(mesosHost string) *Client {
	return &Client{
		client: &http.Client{},
		host:   mesosHost,
	}
}

func (c *Client) GetState() (State, error) {
	stateURL := url.URL{Scheme: "http", Host: c.host, Path: "/state.json"}
	var decodedResponse State
	err := c.do(stateURL, &decodedResponse)
	return decodedResponse, err
}

func (c *Client) do(endpoint url.URL, response interface{}) error {
	req, err := http.NewRequest("GET", endpoint.String(), nil)
	if err != nil {
		return err
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	jsonDecoder := json.NewDecoder(resp.Body)
	if err := jsonDecoder.Decode(&response); err != nil {
		return err
	}
	return nil
}
<commit_msg>update to work with multiple masters:<commit_after>package mesos

import (
	"encoding/json"
	"net/http"
	"net/url"
)

type State struct {
	ActivatedSlaves int64   `json:"activated_slaves"`
	BuildDate       string  `json:"build_date"`
	BuildTime       float64 `json:"build_time"`
	BuildUser       string  `json:"build_user"`
	// CompletedFrameworks    []Something       `json:"completed_frameworks"`
	DeactivatedSlaves int64             `json:"deactivated_slaves"`
	ElectedTime       float64           `json:"elected_time"`
	FailedTasks       int64             `json:"failed_tasks"`
	FinishedTasks     int64             `json:"finished_tasks"`
	Flags             map[string]string `json:"flags"`
	Frameworks        []Framework       `json:"frameworks"`
	GitSHA            string            `json:"git_sha"`
	GitTag            string            `json:"git_tag"`
	Hostname          string            `json:"hostname"`
	ID                string            `json:"id"`
	KilledTasks       int64             `json:"killed_tasks"`
	Leader            string            `json:"leader"`
	LogDir            string            `json:"log_dir"`
	LostTasks         int64             `json:"lost_tasks"`
	// OrphanTasks            []Something       `json:"orphan_tasks"`
	PID          string  `json:"pid"`
	Slaves       []Slave `json:"slaves"`
	StagedTasks  int64   `json:"staged_tasks"`
	StartTime    float64 `json:"start_time"`
	StartedTasks int64   `json:"started_tasks"`
	// UnregisteredFrameworks []Something       `json:"unregistered_frameworks"`
	Version string `json:"version"`
}

type Slave struct {
	Active         bool              `json:"active"`
	Attributes     map[string]string `json:"attributes"`
	Hostname       string            `json:"hostname"`
	ID             string            `json:"id"`
	PID            string            `json:"pid"`
	RegisteredTime float64           `json:"registered_time"`
	Resources      Resources         `json:"resources"`
}

type Resources struct {
	CPUs  float64 `json:"cpus"`
	Disk  int64   `json:"disk"`
	Mem   int64   `json:"mem"`
	Ports string  `json:"ports"`
}

type Framework struct {
	Active           bool        `json:"active"`
	Checkpoint       bool        `json:"checkpoint"`
	CompletedTasks   []Task      `json:"checkpoint"`
	FailoverTimeout  int64       `json:"failover_timeout"`
	Hostname         string      `json:"hostname"`
	ID               string      `json:"id"`
	Name             string      `json:"name"`
	OfferedResources Resources   `json:"offered_resources"`
	Offers           []Resources `json:"offers"`
	RegisteredTime   float64     `json:"registered_time"`
	ReregisteredTime float64     `json:"reregistered_time"`
	Resources        Resources   `json:"resources"`
	Role             string      `json:"role"`
	Tasks            []Task      `json:"tasks"`
	UnregisteredTime float64     `json:"unregistered_time"`
	UsedResources    Resources   `json:"used_resources"`
	User             string      `json:"user"`
	WebUIURL         string      `json:"webui_url"`
}

type Task struct {
	ExecutorID  string    `json:"executor_id"`
	FrameworkID string    `json:"framework_id"`
	ID          string    `json:"id"`
	Labels      []Label   `json:"labels"`
	Name        string    `json:"name"`
	Resources   Resources `json:"resources"`
	SlaveID     string    `json:"slave_id"`
	State       string    `json:"state"`
	Statuses    []Status  `json:"statuses"`
}

type Label struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

type Status struct {
	State     string  `json:"state"`
	Timestamp float64 `json:"timestamp"`
}

type Client struct {
	client *http.Client
	host   string
}

func NewClient(mesosHost string) *Client {
	return &Client{
		client: &http.Client{},
		host:   mesosHost,
	}
}

func (c *Client) GetState() (State, error) {
	stateURL := url.URL{Scheme: "http", Host: c.host, Path: "/state.json"}
	var decodedResponse State
	err := c.do(stateURL, &decodedResponse)
	if err != nil {
		return decodedResponse, err
	}
	// strip "master@" from beginning of leader
	leader := decodedResponse.Leader[7:]

	leaderURL := url.URL{Scheme: "http", Host: leader, Path: "/state.json"}
	err = c.do(leaderURL, &decodedResponse)

	return decodedResponse, err
}

func (c *Client) do(endpoint url.URL, response interface{}) error {
	req, err := http.NewRequest("GET", endpoint.String(), nil)
	if err != nil {
		return err
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	jsonDecoder := json.NewDecoder(resp.Body)
	if err := jsonDecoder.Decode(&response); err != nil {
		return err
	}
	return nil
}
<|endoftext|>


<commit_before>package main

import (
    "os"
    "bufio"
    "fmt"
    "io"
    "math"
    "regexp"
    "sort"
    "strconv"
    "strings"
)

const (
   EFFECTIVE_DIGIT = 3
)

type Measure struct {
  Url string
  Count int
  Total float64
  Min float64
  Mean float64
  Median float64
  P90 float64
  Max float64
}

type By func(a, b *Measure) bool

func (by By) Sort(measures []*Measure) {
  ms := &measureSorter{
    measures: measures,
    by: by,
  }
  sort.Sort(ms)
}

type measureSorter struct {
  measures []*Measure
  by func(a, b *Measure) bool
}

func (s *measureSorter) Len() int {
  return len(s.measures)
}

func (s *measureSorter) Swap(i, j int) {
  s.measures[i], s.measures[j] = s.measures[j], s.measures[i]
}

func (s *measureSorter) Less(i, j int) bool {
  return s.by(s.measures[i], s.measures[j])
}

type Column struct {
  Name string
  Summary string
  Sort By
}

var (
    totals = make(map[string]float64)
    times = make(map[string][]float64)
    measures []*Measure
    topCount = 10
    columns = []*Column{
      &Column{ Name: "Count", Summary: "Count", Sort: func(a, b *Measure) bool { return a.Count > b.Count } },
      &Column{ Name: "Total", Summary: "Total", Sort: func(a, b *Measure) bool { return a.Total > b.Total } },
      &Column{ Name: "Mean", Summary: "Mean", Sort: func(a, b *Measure) bool { return a.Mean > b.Mean } },
      &Column{ Name: "Min", Summary: "Minimum(0 Percentile)", Sort: func(a, b *Measure) bool { return a.Min > b.Min } },
      &Column{ Name: "Median", Summary: "Median(50 Percentile)", Sort: func(a, b *Measure) bool { return a.Median > b.Median } },
      &Column{ Name: "P90", Summary: "90 Percentile", Sort: func(a, b *Measure) bool { return a.P90 > b.P90 } },
      &Column{ Name: "Max", Summary: "Maximum(100 Percentile)", Sort: func(a, b *Measure) bool { return a.Max > b.Max } },
    }
)

func showMeasures(measures []*Measure) {
  countWidth := 5 // for title
  totalWidth := 2 + EFFECTIVE_DIGIT
  maxWidth := 2 + EFFECTIVE_DIGIT

  for i := 0; i < topCount; i++ {
    if countWidth < int(math.Log10(float64(measures[i].Count)) + 1) {
      countWidth = int(math.Log10(float64(measures[i].Count)) + 1)
    }
    if totalWidth < int(math.Log10(measures[i].Total) + 1) {
      totalWidth = int(math.Log10(measures[i].Total) + 1)
    }
    if maxWidth < int(math.Log10(measures[i].Max) + 1) {
      maxWidth = int(math.Log10(measures[i].Max) + 1)
    }
  }

  var format string
  for _, column := range columns {
    switch column.Name {
    case "Count":
      fmt.Printf(fmt.Sprintf("%%%ds ", countWidth), column.Name)
      format += fmt.Sprintf("%%%dd ", countWidth)
    case "Total":
      fmt.Printf(fmt.Sprintf("%%%ds ", totalWidth + EFFECTIVE_DIGIT + 1), column.Name)
      format += fmt.Sprintf("%%%d.%df ", totalWidth + EFFECTIVE_DIGIT + 1, EFFECTIVE_DIGIT)
    default:
      fmt.Printf(fmt.Sprintf("%%%ds ", maxWidth + EFFECTIVE_DIGIT + 1), column.Name)
      format += fmt.Sprintf("%%%d.%df ", maxWidth + EFFECTIVE_DIGIT + 1, EFFECTIVE_DIGIT)
    }
  }
  fmt.Printf("url\n")
  format += "%s\n"

  for i := 0; i < topCount; i++ {
    m := measures[i]
    fmt.Printf(format, m.Count, m.Total, m.Mean, m.Min, m.Median, m.P90, m.Max, m.Url)
  }
}

func main() {
    reader := bufio.NewReaderSize(os.Stdin, 4096)
    delimiter := regexp.MustCompile(" +")
    for {
        line, err := reader.ReadString('\n')
        if err == io.EOF {
          break
        } else if err != nil {
          panic(err)
        }
        s := delimiter.Split(line, -1)
        if len(s) > 0 {
          var url string
          if len(s) >= 7 {
            url = strings.TrimLeft(strings.Join(s[5:7], " "), "\"")
          }
          time, err := strconv.ParseFloat(strings.Trim(s[len(s)-1], "\r\n"), 10)
          if err != nil {
            time = 0.000
          }
          // time /= 1000000 // for Apache
          totals[url] += time
          times[url] = append(times[url], time)
        }
    }

    for url, total := range totals {
      sorted := times[url]
      sort.Float64s(sorted)
      count := len(sorted)
      measure := &Measure{
        Url: url,
        Count: count,
        Total: total,
        Min: sorted[0],
        Mean: totals[url]/float64(count),
        Median: sorted[int(count*50/100)],
        P90: sorted[int(count*90/100)],
        Max: sorted[count-1],
      }
      measures = append(measures, measure)
    }
    if len(measures) < topCount {
      topCount = len(measures)
    }

    for _, column := range columns {
      fmt.Printf("Sort By %s\n", column.Summary)
      By(column.Sort).Sort(measures)
      showMeasures(measures)
      fmt.Println()
    }
}
<commit_msg>桁計算が間違っていたので修正。見やすくするために間を空白2つに<commit_after>package main

import (
    "os"
    "bufio"
    "fmt"
    "io"
    "math"
    "regexp"
    "sort"
    "strconv"
    "strings"
)

const (
   EFFECTIVE_DIGIT = 3
)

type Measure struct {
  Url string
  Count int
  Total float64
  Min float64
  Mean float64
  Median float64
  P90 float64
  Max float64
}

type By func(a, b *Measure) bool

func (by By) Sort(measures []*Measure) {
  ms := &measureSorter{
    measures: measures,
    by: by,
  }
  sort.Sort(ms)
}

type measureSorter struct {
  measures []*Measure
  by func(a, b *Measure) bool
}

func (s *measureSorter) Len() int {
  return len(s.measures)
}

func (s *measureSorter) Swap(i, j int) {
  s.measures[i], s.measures[j] = s.measures[j], s.measures[i]
}

func (s *measureSorter) Less(i, j int) bool {
  return s.by(s.measures[i], s.measures[j])
}

type Column struct {
  Name string
  Summary string
  Sort By
}

var (
    totals = make(map[string]float64)
    times = make(map[string][]float64)
    measures []*Measure
    topCount = 10
    columns = []*Column{
      &Column{ Name: "Count", Summary: "Count", Sort: func(a, b *Measure) bool { return a.Count > b.Count } },
      &Column{ Name: "Total", Summary: "Total", Sort: func(a, b *Measure) bool { return a.Total > b.Total } },
      &Column{ Name: "Mean", Summary: "Mean", Sort: func(a, b *Measure) bool { return a.Mean > b.Mean } },
      &Column{ Name: "Min", Summary: "Minimum(0 Percentile)", Sort: func(a, b *Measure) bool { return a.Min > b.Min } },
      &Column{ Name: "Median", Summary: "Median(50 Percentile)", Sort: func(a, b *Measure) bool { return a.Median > b.Median } },
      &Column{ Name: "P90", Summary: "90 Percentile", Sort: func(a, b *Measure) bool { return a.P90 > b.P90 } },
      &Column{ Name: "Max", Summary: "Maximum(100 Percentile)", Sort: func(a, b *Measure) bool { return a.Max > b.Max } },
    }
)

func showMeasures(measures []*Measure) {
  countWidth := 5 // for title
  totalWidth := 2 + EFFECTIVE_DIGIT
  maxWidth := 2 + EFFECTIVE_DIGIT

  for i := 0; i < topCount; i++ {
    if countWidth < int(math.Log10(float64(measures[i].Count)) + 1) {
      countWidth = int(math.Log10(float64(measures[i].Count)) + 1)
    }
    if totalWidth < int(math.Log10(measures[i].Total) + 1 + EFFECTIVE_DIGIT + 1) {
      totalWidth = int(math.Log10(measures[i].Total) + 1 + EFFECTIVE_DIGIT + 1)
    }
    if maxWidth < int(math.Log10(measures[i].Max) + 1 + EFFECTIVE_DIGIT + 1) {
      maxWidth = int(math.Log10(measures[i].Max) + 1 + EFFECTIVE_DIGIT + 1)
    }
  }

  var format string
  for _, column := range columns {
    switch column.Name {
    case "Count":
      fmt.Printf(fmt.Sprintf("%%%ds  ", countWidth), column.Name)
      format += fmt.Sprintf("%%%dd  ", countWidth)
    case "Total":
      fmt.Printf(fmt.Sprintf("%%%ds  ", totalWidth), column.Name)
      format += fmt.Sprintf("%%%d.%df  ", totalWidth, EFFECTIVE_DIGIT)
    default:
      fmt.Printf(fmt.Sprintf("%%%ds  ", maxWidth), column.Name)
      format += fmt.Sprintf("%%%d.%df  ", maxWidth, EFFECTIVE_DIGIT)
    }
  }
  fmt.Printf("url\n")
  format += "%s\n"

  for i := 0; i < topCount; i++ {
    m := measures[i]
    fmt.Printf(format, m.Count, m.Total, m.Mean, m.Min, m.Median, m.P90, m.Max, m.Url)
  }
}

func main() {
    reader := bufio.NewReaderSize(os.Stdin, 4096)
    delimiter := regexp.MustCompile(" +")
    for {
        line, err := reader.ReadString('\n')
        if err == io.EOF {
          break
        } else if err != nil {
          panic(err)
        }
        s := delimiter.Split(line, -1)
        if len(s) > 0 {
          var url string
          if len(s) >= 7 {
            url = strings.TrimLeft(strings.Join(s[5:7], " "), "\"")
          }
          time, err := strconv.ParseFloat(strings.Trim(s[len(s)-1], "\r\n"), 10)
          if err != nil {
            time = 0.000
          }
          // time /= 1000000 // for Apache
          totals[url] += time
          times[url] = append(times[url], time)
        }
    }

    for url, total := range totals {
      sorted := times[url]
      sort.Float64s(sorted)
      count := len(sorted)
      measure := &Measure{
        Url: url,
        Count: count,
        Total: total,
        Min: sorted[0],
        Mean: totals[url]/float64(count),
        Median: sorted[int(count*50/100)],
        P90: sorted[int(count*90/100)],
        Max: sorted[count-1],
      }
      measures = append(measures, measure)
    }
    if len(measures) < topCount {
      topCount = len(measures)
    }

    for _, column := range columns {
      fmt.Printf("Sort By %s\n", column.Summary)
      By(column.Sort).Sort(measures)
      showMeasures(measures)
      fmt.Println()
    }
}
<|endoftext|>


<commit_before>package stylecheck // import "honnef.co/go/tools/stylecheck"

import (
	"fmt"
	"go/ast"
	"go/constant"
	"go/token"
	"go/types"
	"strings"
	"unicode"
	"unicode/utf8"

	"honnef.co/go/tools/lint"
	"honnef.co/go/tools/ssa"

	"golang.org/x/tools/go/types/typeutil"
)

type Checker struct {
	CheckGenerated bool
}

func NewChecker() *Checker {
	return &Checker{}
}

func (*Checker) Name() string   { return "stylecheck" }
func (*Checker) Prefix() string { return "ST" }

func (c *Checker) Init(prog *lint.Program) {
}

func (c *Checker) filterGenerated(files []*ast.File) []*ast.File {
	if c.CheckGenerated {
		return files
	}
	var out []*ast.File
	for _, f := range files {
		if !lint.IsGenerated(f) {
			out = append(out, f)
		}
	}
	return out
}

func (c *Checker) Funcs() map[string]lint.Func {
	return map[string]lint.Func{
		"ST1000": c.CheckPackageComment,
		"ST1001": c.CheckDotImports,
		"ST1002": nil, // XXX missing/malformed comments for exported identifiers
		"ST1003": nil, // XXX underscores in names
		"ST1004": nil, // XXX incorrect initialisms
		"ST1005": c.CheckErrorStrings,
		"ST1006": c.CheckReceiverNames,
		"ST1007": c.CheckIncDec,
		"ST1008": c.CheckErrorReturn,
		"ST1009": c.CheckUnexportedReturn,
		"ST1010": c.CheckContextFirstArg,
	}
}

func (c *Checker) CheckPackageComment(j *lint.Job) {
	// - At least one file in a package should have a package comment
	//
	// - For non-main packages, the comment should be of the form
	// "Package x ...". This has a slight potential for false
	// positives, as multiple files can have package comments, in
	// which case they get appended. But that doesn't happen a lot in
	// the real world.

	for _, pkg := range j.Program.Packages {
		hasDocs := false
		for _, f := range pkg.Info.Files {
			if j.IsInTest(f) {
				continue
			}
			if f.Doc != nil && len(f.Doc.List) > 0 {
				hasDocs = true
				if f.Name.Name != "main" {
					prefix := "Package " + f.Name.Name + " "
					if !strings.HasPrefix(strings.TrimSpace(f.Doc.Text()), prefix) {
						j.Errorf(f.Doc, `package comment should be of the form "%s..."`, prefix)
					}
				}
				f.Doc.Text()
			}
		}

		if !hasDocs {
			for _, f := range pkg.Info.Files {
				if j.IsInTest(f) {
					continue
				}
				j.Errorf(f, "at least one file in a package should have a package comment")
			}
		}
	}
}

func (c *Checker) CheckDotImports(j *lint.Job) {
	for _, pkg := range j.Program.Packages {
		for _, f := range pkg.Info.Files {
			for _, imp := range f.Imports {
				if imp.Name != nil && imp.Name.Name == "." && !j.IsInTest(f) {
					j.Errorf(imp, "should not use dot imports")
				}
			}
		}
	}
}

func (c *Checker) CheckIncDec(j *lint.Job) {
	fn := func(node ast.Node) bool {
		assign, ok := node.(*ast.AssignStmt)
		if !ok || (assign.Tok != token.ADD_ASSIGN && assign.Tok != token.SUB_ASSIGN) {
			return true
		}
		if (len(assign.Lhs) != 1 || len(assign.Rhs) != 1) ||
			!lint.IsIntLiteral(assign.Rhs[0], "1") {
			return true
		}

		suffix := ""
		switch assign.Tok {
		case token.ADD_ASSIGN:
			suffix = "++"
		case token.SUB_ASSIGN:
			suffix = "--"
		}

		j.Errorf(assign, "should replace %s with %s%s", j.Render(assign), j.Render(assign.Lhs[0]), suffix)
		return true
	}
	for _, f := range c.filterGenerated(j.Program.Files) {
		ast.Inspect(f, fn)
	}
}

func (c *Checker) CheckErrorReturn(j *lint.Job) {
fnLoop:
	for _, fn := range j.Program.InitialFunctions {
		sig := fn.Type().(*types.Signature)
		rets := sig.Results()
		if rets == nil || rets.Len() < 2 {
			continue
		}

		if rets.At(rets.Len()-1).Type() == types.Universe.Lookup("error").Type() {
			// Last return type is error. If the function also returns
			// errors in other positions, that's fine.
			continue
		}
		for i := rets.Len() - 2; i >= 0; i-- {
			if rets.At(i).Type() == types.Universe.Lookup("error").Type() {
				j.Errorf(rets.At(i), "error should be returned as the last argument")
				continue fnLoop
			}
		}
	}
}

// CheckUnexportedReturn checks that exported functions on exported
// types do not return unexported types.
func (c *Checker) CheckUnexportedReturn(j *lint.Job) {
	for _, fn := range j.Program.InitialFunctions {
		if fn.Synthetic != "" || fn.Parent() != nil {
			continue
		}
		if !ast.IsExported(fn.Name()) || j.IsInMain(fn) || j.IsInTest(fn) {
			continue
		}
		sig := fn.Type().(*types.Signature)
		if sig.Recv() != nil && !ast.IsExported(lint.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) {
			continue
		}
		res := sig.Results()
		for i := 0; i < res.Len(); i++ {
			if named, ok := lint.DereferenceR(res.At(i).Type()).(*types.Named); ok &&
				!ast.IsExported(named.Obj().Name()) &&
				named != types.Universe.Lookup("error").Type() {
				j.Errorf(fn, "should not return unexported type")
			}
		}
	}
}

func (c *Checker) CheckReceiverNames(j *lint.Job) {
	for _, pkg := range j.Program.Packages {
		for _, m := range pkg.Members {
			names := map[string]int{}

			var firstFn *types.Func
			if T, ok := m.Object().(*types.TypeName); ok {
				ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
				for _, sel := range ms {
					fn := sel.Obj().(*types.Func)
					recv := fn.Type().(*types.Signature).Recv()
					if lint.Dereference(recv.Type()) != T.Type() {
						// skip embedded methods
						continue
					}
					if firstFn == nil {
						firstFn = fn
					}
					if recv.Name() != "" && recv.Name() != "_" {
						names[recv.Name()]++
					}
					if recv.Name() == "self" || recv.Name() == "this" {
						j.Errorf(recv, `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`)
					}
					if recv.Name() == "_" {
						j.Errorf(recv, "receiver name should not be an underscore, omit the name if it is unused")
					}
				}
			}

			if len(names) > 1 {
				var seen []string
				for name, count := range names {
					seen = append(seen, fmt.Sprintf("%dx %q", count, name))
				}

				j.Errorf(firstFn, "methods on the same type should have the same receiver name (seen %s)", strings.Join(seen, ", "))
			}
		}
	}
}

func (c *Checker) CheckContextFirstArg(j *lint.Job) {
fnLoop:
	for _, fn := range j.Program.InitialFunctions {
		if fn.Synthetic != "" || fn.Parent() != nil {
			continue
		}
		params := fn.Signature.Params()
		if params.Len() < 2 {
			continue
		}
		if types.TypeString(params.At(0).Type(), nil) == "context.Context" {
			continue
		}
		for i := 1; i < params.Len(); i++ {
			param := params.At(i)
			if types.TypeString(param.Type(), nil) == "context.Context" {
				j.Errorf(param, "context.Context should be the first argument of a function")
				continue fnLoop
			}
		}
	}
}

func (c *Checker) CheckErrorStrings(j *lint.Job) {
	for _, fn := range j.Program.InitialFunctions {
		if j.IsInTest(fn) {
			// We don't care about malformed error messages in tests;
			// they're usually for direct human consumption, not part
			// of an API
			continue
		}
		for _, block := range fn.Blocks {
			for _, ins := range block.Instrs {
				call, ok := ins.(*ssa.Call)
				if !ok {
					continue
				}
				if !lint.IsCallTo(call.Common(), "errors.New") && !lint.IsCallTo(call.Common(), "fmt.Errorf") {
					continue
				}

				k, ok := call.Common().Args[0].(*ssa.Const)
				if !ok {
					continue
				}

				s := constant.StringVal(k.Value)
				if len(s) == 0 {
					continue
				}
				switch s[len(s)-1] {
				case '.', ':', '!', '\n':
					j.Errorf(call, "error strings should not end with punctuation or a newline")
				}
				idx := strings.IndexByte(s, ' ')
				if idx == -1 {
					// single word error message, probably not a real
					// error but something used in tests or during
					// debugging
					continue
				}
				word := s[:idx]
				first, _ := utf8.DecodeRuneInString(word)
				if !unicode.IsUpper(first) {
					continue
				}
				many := false
				for _, c := range []rune(word)[1:] {
					if unicode.IsUpper(c) {
						many = true
						break
					}
				}
				if !many {
					// First word in error starts with a capital
					// letter, and the word doesn't contain any other
					// capitals, making it unlikely to be an
					// initialism or multi-word function name.
					//
					// It could still be a single-word function name, though.
					j.Errorf(call, "error strings should not be capitalized")
				}
			}
		}
	}
}
<commit_msg>stylecheck: document corner cases<commit_after>package stylecheck // import "honnef.co/go/tools/stylecheck"

import (
	"fmt"
	"go/ast"
	"go/constant"
	"go/token"
	"go/types"
	"strings"
	"unicode"
	"unicode/utf8"

	"honnef.co/go/tools/lint"
	"honnef.co/go/tools/ssa"

	"golang.org/x/tools/go/types/typeutil"
)

type Checker struct {
	CheckGenerated bool
}

func NewChecker() *Checker {
	return &Checker{}
}

func (*Checker) Name() string   { return "stylecheck" }
func (*Checker) Prefix() string { return "ST" }

func (c *Checker) Init(prog *lint.Program) {
}

func (c *Checker) filterGenerated(files []*ast.File) []*ast.File {
	if c.CheckGenerated {
		return files
	}
	var out []*ast.File
	for _, f := range files {
		if !lint.IsGenerated(f) {
			out = append(out, f)
		}
	}
	return out
}

func (c *Checker) Funcs() map[string]lint.Func {
	return map[string]lint.Func{
		"ST1000": c.CheckPackageComment,
		"ST1001": c.CheckDotImports,
		"ST1002": nil, // XXX missing/malformed comments for exported identifiers
		"ST1003": nil, // XXX underscores in names
		"ST1004": nil, // XXX incorrect initialisms
		"ST1005": c.CheckErrorStrings,
		"ST1006": c.CheckReceiverNames,
		"ST1007": c.CheckIncDec,
		"ST1008": c.CheckErrorReturn,
		"ST1009": c.CheckUnexportedReturn,
		"ST1010": c.CheckContextFirstArg,
	}
}

func (c *Checker) CheckPackageComment(j *lint.Job) {
	// - At least one file in a package should have a package comment
	//
	// - For non-main packages, the comment should be of the form
	// "Package x ...". This has a slight potential for false
	// positives, as multiple files can have package comments, in
	// which case they get appended. But that doesn't happen a lot in
	// the real world.

	for _, pkg := range j.Program.Packages {
		hasDocs := false
		for _, f := range pkg.Info.Files {
			if j.IsInTest(f) {
				continue
			}
			if f.Doc != nil && len(f.Doc.List) > 0 {
				hasDocs = true
				if f.Name.Name != "main" {
					prefix := "Package " + f.Name.Name + " "
					if !strings.HasPrefix(strings.TrimSpace(f.Doc.Text()), prefix) {
						j.Errorf(f.Doc, `package comment should be of the form "%s..."`, prefix)
					}
				}
				f.Doc.Text()
			}
		}

		if !hasDocs {
			for _, f := range pkg.Info.Files {
				if j.IsInTest(f) {
					continue
				}
				j.Errorf(f, "at least one file in a package should have a package comment")
			}
		}
	}
}

func (c *Checker) CheckDotImports(j *lint.Job) {
	for _, pkg := range j.Program.Packages {
		for _, f := range pkg.Info.Files {
			for _, imp := range f.Imports {
				if imp.Name != nil && imp.Name.Name == "." && !j.IsInTest(f) {
					j.Errorf(imp, "should not use dot imports")
				}
			}
		}
	}
}

func (c *Checker) CheckIncDec(j *lint.Job) {
	// TODO(dh): this can be noisy for function bodies that look like this:
	// 	x += 3
	// 	...
	// 	x += 2
	// 	...
	// 	x += 1
	fn := func(node ast.Node) bool {
		assign, ok := node.(*ast.AssignStmt)
		if !ok || (assign.Tok != token.ADD_ASSIGN && assign.Tok != token.SUB_ASSIGN) {
			return true
		}
		if (len(assign.Lhs) != 1 || len(assign.Rhs) != 1) ||
			!lint.IsIntLiteral(assign.Rhs[0], "1") {
			return true
		}

		suffix := ""
		switch assign.Tok {
		case token.ADD_ASSIGN:
			suffix = "++"
		case token.SUB_ASSIGN:
			suffix = "--"
		}

		j.Errorf(assign, "should replace %s with %s%s", j.Render(assign), j.Render(assign.Lhs[0]), suffix)
		return true
	}
	for _, f := range c.filterGenerated(j.Program.Files) {
		ast.Inspect(f, fn)
	}
}

func (c *Checker) CheckErrorReturn(j *lint.Job) {
fnLoop:
	for _, fn := range j.Program.InitialFunctions {
		sig := fn.Type().(*types.Signature)
		rets := sig.Results()
		if rets == nil || rets.Len() < 2 {
			continue
		}

		if rets.At(rets.Len()-1).Type() == types.Universe.Lookup("error").Type() {
			// Last return type is error. If the function also returns
			// errors in other positions, that's fine.
			continue
		}
		for i := rets.Len() - 2; i >= 0; i-- {
			if rets.At(i).Type() == types.Universe.Lookup("error").Type() {
				j.Errorf(rets.At(i), "error should be returned as the last argument")
				continue fnLoop
			}
		}
	}
}

// CheckUnexportedReturn checks that exported functions on exported
// types do not return unexported types.
func (c *Checker) CheckUnexportedReturn(j *lint.Job) {
	for _, fn := range j.Program.InitialFunctions {
		if fn.Synthetic != "" || fn.Parent() != nil {
			continue
		}
		if !ast.IsExported(fn.Name()) || j.IsInMain(fn) || j.IsInTest(fn) {
			continue
		}
		sig := fn.Type().(*types.Signature)
		if sig.Recv() != nil && !ast.IsExported(lint.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) {
			continue
		}
		res := sig.Results()
		for i := 0; i < res.Len(); i++ {
			if named, ok := lint.DereferenceR(res.At(i).Type()).(*types.Named); ok &&
				!ast.IsExported(named.Obj().Name()) &&
				named != types.Universe.Lookup("error").Type() {
				j.Errorf(fn, "should not return unexported type")
			}
		}
	}
}

func (c *Checker) CheckReceiverNames(j *lint.Job) {
	for _, pkg := range j.Program.Packages {
		for _, m := range pkg.Members {
			names := map[string]int{}

			var firstFn *types.Func
			if T, ok := m.Object().(*types.TypeName); ok {
				ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
				for _, sel := range ms {
					fn := sel.Obj().(*types.Func)
					recv := fn.Type().(*types.Signature).Recv()
					if lint.Dereference(recv.Type()) != T.Type() {
						// skip embedded methods
						continue
					}
					if firstFn == nil {
						firstFn = fn
					}
					if recv.Name() != "" && recv.Name() != "_" {
						names[recv.Name()]++
					}
					if recv.Name() == "self" || recv.Name() == "this" {
						j.Errorf(recv, `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`)
					}
					if recv.Name() == "_" {
						j.Errorf(recv, "receiver name should not be an underscore, omit the name if it is unused")
					}
				}
			}

			if len(names) > 1 {
				var seen []string
				for name, count := range names {
					seen = append(seen, fmt.Sprintf("%dx %q", count, name))
				}

				j.Errorf(firstFn, "methods on the same type should have the same receiver name (seen %s)", strings.Join(seen, ", "))
			}
		}
	}
}

func (c *Checker) CheckContextFirstArg(j *lint.Job) {
	// TODO(dh): this check doesn't apply to test helpers. Example from the stdlib:
	// 	func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) {
fnLoop:
	for _, fn := range j.Program.InitialFunctions {
		if fn.Synthetic != "" || fn.Parent() != nil {
			continue
		}
		params := fn.Signature.Params()
		if params.Len() < 2 {
			continue
		}
		if types.TypeString(params.At(0).Type(), nil) == "context.Context" {
			continue
		}
		for i := 1; i < params.Len(); i++ {
			param := params.At(i)
			if types.TypeString(param.Type(), nil) == "context.Context" {
				j.Errorf(param, "context.Context should be the first argument of a function")
				continue fnLoop
			}
		}
	}
}

func (c *Checker) CheckErrorStrings(j *lint.Job) {
	for _, fn := range j.Program.InitialFunctions {
		if j.IsInTest(fn) {
			// We don't care about malformed error messages in tests;
			// they're usually for direct human consumption, not part
			// of an API
			continue
		}
		for _, block := range fn.Blocks {
			for _, ins := range block.Instrs {
				call, ok := ins.(*ssa.Call)
				if !ok {
					continue
				}
				if !lint.IsCallTo(call.Common(), "errors.New") && !lint.IsCallTo(call.Common(), "fmt.Errorf") {
					continue
				}

				k, ok := call.Common().Args[0].(*ssa.Const)
				if !ok {
					continue
				}

				s := constant.StringVal(k.Value)
				if len(s) == 0 {
					continue
				}
				switch s[len(s)-1] {
				case '.', ':', '!', '\n':
					j.Errorf(call, "error strings should not end with punctuation or a newline")
				}
				idx := strings.IndexByte(s, ' ')
				if idx == -1 {
					// single word error message, probably not a real
					// error but something used in tests or during
					// debugging
					continue
				}
				word := s[:idx]
				first, _ := utf8.DecodeRuneInString(word)
				if !unicode.IsUpper(first) {
					continue
				}
				many := false
				for _, c := range []rune(word)[1:] {
					if unicode.IsUpper(c) {
						many = true
						break
					}
				}
				if !many {
					// First word in error starts with a capital
					// letter, and the word doesn't contain any other
					// capitals, making it unlikely to be an
					// initialism or multi-word function name.
					//
					// It could still be a single-word function name
					// or a proper noun, though.
					//
					// TODO(dh): example from the stdlib that we incorrectly flag:
					// 	func (w *pooledFlateWriter) Write(p []byte) (n int, err error) {
					// 		...
					// 		return 0, errors.New("Write after Close")
					// 		...
					// 	}

					j.Errorf(call, "error strings should not be capitalized")
				}
			}
		}
	}
}
<|endoftext|>


<commit_before>package consumergroup

import (
	"errors"
	"fmt"
	"reflect"
	"runtime/debug"
	"sync"
	"time"

	"github.com/Shopify/sarama"
	"github.com/meitu/go-zookeeper/zk"
	"github.com/sirupsen/logrus"
)

const (
	cgInit = iota
	cgStart
	cgStopped
)

const (
	restartEvent = iota
	quitEvent
)

// ConsumerGroup consume message from Kafka with rebalancing supports
type ConsumerGroup struct {
	name            string
	storage         groupStorage
	topicConsumers  map[string]*topicConsumer
	saramaClients   map[string]sarama.Client
	saramaConsumers map[string]sarama.Consumer

	id          string
	state       int
	wg          sync.WaitGroup
	stopCh      chan struct{}
	triggerCh   chan int
	triggerOnce *sync.Once
	owners      map[string]map[int32]string

	config *Config
	logger *logrus.Logger

	onLoad, onClose []func()
}

// NewConsumerGroup create the ConsumerGroup instance with config
func NewConsumerGroup(config *Config) (*ConsumerGroup, error) {
	if config == nil {
		return nil, errors.New("config can't be empty")
	}
	err := config.validate()
	if err != nil {
		return nil, fmt.Errorf("vaildate config failed, as %s", err)
	}

	cg := new(ConsumerGroup)
	cg.state = cgInit
	cg.config = config
	cg.id = config.ConsumerID
	if cg.id == "" {
		cg.id = genConsumerID()
	}
	cg.name = config.GroupID
	cg.triggerCh = make(chan int)
	cg.topicConsumers = make(map[string]*topicConsumer)
	cg.saramaClients = make(map[string]sarama.Client)
	cg.saramaConsumers = make(map[string]sarama.Consumer)
	cg.onLoad = make([]func(), 0)
	cg.onClose = make([]func(), 0)
	cg.storage = newZKGroupStorage(config.ZkList, config.ZkSessionTimeout)
	cg.logger = logrus.New()
	if _, ok := cg.storage.(*zkGroupStorage); ok {
		cg.storage.(*zkGroupStorage).Chroot(config.Chroot)
	}

	err = cg.initSaramaConsumer()
	if err != nil {
		return nil, fmt.Errorf("init sarama consumer, as %s", err)
	}
	cg.owners = make(map[string]map[int32]string)
	for _, topic := range config.TopicList {
		cg.topicConsumers[topic] = newTopicConsumer(cg, topic)
		cg.owners[topic] = make(map[int32]string)
	}
	return cg, nil
}

func (cg *ConsumerGroup) initSaramaConsumer() error {
	brokerList, err := cg.storage.getBrokerList()
	if err != nil {
		return err
	}
	if len(brokerList) == 0 {
		return errors.New("no broker alive")
	}
	for _, topic := range cg.config.TopicList {
		saramaClient, err := sarama.NewClient(brokerList, cg.config.SaramaConfig)
		if err != nil {
			return err
		}
		saramaConsumer, err := sarama.NewConsumerFromClient(saramaClient)
		if err != nil {
			return err
		}
		cg.saramaClients[topic] = saramaClient
		cg.saramaConsumers[topic] = saramaConsumer
	}
	return nil
}

// Start would register ConsumerGroup, and rebalance would be triggered.
// ConsumerGroup computes the partitions which should be consumed by consumer's num, and start fetching message.
func (cg *ConsumerGroup) Start() error {
	// exit when failed to register the consumer
	err := cg.storage.registerConsumer(cg.name, cg.id, nil)
	if err != nil && err != zk.ErrNodeExists {
		return err
	}
	cg.wg.Add(1)
	go cg.start()
	return nil
}

// Stop would unregister ConsumerGroup, and rebalance would be triggered.
// The partitions which consumed by this ConsumerGroup would be assigned to others.
func (cg *ConsumerGroup) Stop() {
	cg.stop()
	cg.wg.Wait()
}

// SetLogger use to set the user's logger the consumer group
func (cg *ConsumerGroup) SetLogger(l *logrus.Logger) {
	if l != nil {
		cg.logger = l
	}
}

// IsStopped return whether the ConsumerGroup was stopped or not.
func (cg *ConsumerGroup) IsStopped() bool {
	return cg.state == cgStopped
}

func (cg *ConsumerGroup) callRecover() {
	if err := recover(); err != nil {
		cg.logger.WithFields(logrus.Fields{
			"group": cg.name,
			"err":   err,
			"stack": string(debug.Stack()),
		}).Error("Recover panic")
		cg.stop()
	}
}

func (cg *ConsumerGroup) start() {
	var wg sync.WaitGroup

	defer cg.callRecover()
	defer func() {
		cg.state = cgStopped
		err := cg.storage.deleteConsumer(cg.name, cg.id)
		if err != nil {
			cg.logger.WithFields(logrus.Fields{
				"group": cg.name,
				"err":   err,
			}).Error("Failed to delete consumer from zk")
		}
		for _, tc := range cg.topicConsumers {
			close(tc.messages)
			close(tc.errors)
		}
		cg.wg.Done()
	}()

CONSUME_TOPIC_LOOP:
	for {
		cg.logger.WithField("group", cg.name).Info("Consumer group started")
		cg.triggerOnce = new(sync.Once)
		cg.stopCh = make(chan struct{})

		err := cg.watchRebalance()
		if err != nil {
			cg.logger.WithFields(logrus.Fields{
				"group": cg.name,
				"err":   err,
			}).Error("Failed to watch rebalance")
			cg.stop()
			return
		}
		wg.Add(1)
		go func() {
			defer cg.callRecover()
			defer wg.Done()
			cg.autoReconnect(cg.storage.(*zkGroupStorage).sessionTimeout / 3)
		}()
		for _, consumer := range cg.topicConsumers {
			wg.Add(1)
			consumer.start()
			go func(tc *topicConsumer) {
				defer cg.callRecover()
				defer wg.Done()
				tc.wg.Wait()
				cg.logger.WithFields(logrus.Fields{
					"group": tc.group,
					"topic": tc.name,
				}).Info("Stop the topic consumer")
			}(consumer)
		}
		cg.state = cgStart
		for _, onLoadFunc := range cg.onLoad {
			onLoadFunc()
		}
		msg := <-cg.triggerCh
		for _, onCloseFunc := range cg.onClose {
			onCloseFunc()
		}
		switch msg {
		case restartEvent:
			close(cg.stopCh)
			// The stop channel was used to notify partition's consumer to stop consuming when rebalance is triggered.
			// So we should reinit when rebalance was triggered, as it would be closed.
			wg.Wait()
			continue CONSUME_TOPIC_LOOP
		case quitEvent:
			close(cg.stopCh)
			cg.logger.WithField("group", cg.name).Info("ConsumerGroup is stopping")
			wg.Wait()
			cg.logger.WithField("group", cg.name).Info("ConsumerGroup was stopped")
			return
		}
	}
}

func (cg *ConsumerGroup) stop() {
	cg.triggerOnce.Do(func() { cg.triggerCh <- quitEvent })
}

func (cg *ConsumerGroup) triggerRebalance() {
	cg.triggerOnce.Do(func() { cg.triggerCh <- restartEvent })
}

func (cg *ConsumerGroup) getPartitionConsumer(topic string, partition int32, nextOffset int64) (sarama.PartitionConsumer, error) {
	saramaConsumer := cg.saramaConsumers[topic]
	consumer, err := saramaConsumer.ConsumePartition(topic, partition, nextOffset)
	if err == sarama.ErrOffsetOutOfRange {
		nextOffset = cg.config.OffsetAutoReset
		cg.logger.WithFields(logrus.Fields{
			"group":     cg.name,
			"topic":     topic,
			"partition": partition,
			"offset":    nextOffset,
		}).Error("Partition's offset was out of range, use auto-reset")
		consumer, err = saramaConsumer.ConsumePartition(topic, partition, nextOffset)
	}
	return consumer, err
}

// GetMessages was used to get a unbuffered message's channel from specified topic
func (cg *ConsumerGroup) GetMessages(topic string) (<-chan *sarama.ConsumerMessage, bool) {
	if topicConsumer, ok := cg.topicConsumers[topic]; ok {
		return topicConsumer.messages, true
	}
	return nil, false
}

// GetErrors was used to get a unbuffered error's channel from specified topic
func (cg *ConsumerGroup) GetErrors(topic string) (<-chan *sarama.ConsumerError, bool) {
	if topicConsumer, ok := cg.topicConsumers[topic]; ok {
		return topicConsumer.errors, true
	}
	return nil, false
}

// OnLoad load callback function that runs after startup
func (cg *ConsumerGroup) OnLoad(cb func()) {
	cg.onLoad = append(cg.onLoad, cb)
}

// OnClose load callback function that runs before the end
func (cg *ConsumerGroup) OnClose(cb func()) {
	cg.onClose = append(cg.onClose, cb)
}

func (cg *ConsumerGroup) autoReconnect(interval time.Duration) {
	timer := time.NewTimer(interval)
	cg.logger.WithField("group", cg.name).Info("The auto-reconnect consumer thread was started")
	defer cg.logger.WithField("group", cg.name).Info("The auto-reconnect consumer thread was stopped")
	for {
		select {
		case <-cg.stopCh:
			return
		case <-timer.C:
			timer.Reset(interval)
			exist, err := cg.storage.existsConsumer(cg.name, cg.id)
			if err != nil {
				cg.logger.WithFields(logrus.Fields{
					"group": cg.name,
					"err":   err,
				}).Error("Failed to check consumer existence")
				break
			}
			if exist {
				break
			}
			err = cg.storage.registerConsumer(cg.name, cg.id, nil)
			if err != nil {
				cg.logger.WithFields(logrus.Fields{
					"group": cg.name,
					"err":   err,
				}).Error("Failed to re-register consumer")
			}
		}
	}
}

func (cg *ConsumerGroup) watchRebalance() error {
	consumersWatcher, err := cg.storage.watchConsumerList(cg.name)
	if err != nil {
		return err
	}
	topicsChange, topicWatchers, err := cg.watchTopics(cg.config.TopicList)
	if err != nil {
		return err
	}
	go func() {
		defer cg.callRecover()
		cg.logger.WithField("group", cg.name).Info("Rebalance watcher thread was started")
		select {
		case <-consumersWatcher.EvCh:
			cg.triggerRebalance()
			cg.logger.WithField("group", cg.name).Info("Trigger rebalance while consumers was changed")
			for _, tw := range topicWatchers {
				cg.storage.removeWatcher(tw)
			}
		case topic := <-topicsChange:
			cg.triggerRebalance()
			cg.logger.WithFields(logrus.Fields{
				"group": cg.name,
				"topic": topic,
			}).Info("Trigger rebalance while partitions was changed")
			cg.storage.removeWatcher(consumersWatcher)
		case <-cg.stopCh:
		}
		cg.logger.WithField("group", cg.name).Info("Rebalance watcher thread was exited")
	}()
	return nil
}

// CommitOffset is used to commit offset when auto commit was disabled.
func (cg *ConsumerGroup) CommitOffset(topic string, partition int32, offset int64) error {
	if cg.config.OffsetAutoCommitEnable {
		return errors.New("commit offset take effect when offset auto commit was disabled")
	}
	return cg.storage.commitOffset(cg.name, topic, partition, offset)
}

// GetOffsets return the offset in memory for debug
func (cg *ConsumerGroup) GetOffsets() map[string]interface{} {
	topics := make(map[string]interface{})
	for topic, tc := range cg.topicConsumers {
		topics[topic] = tc.getOffsets()
	}
	return topics
}

// Owners return owners of all partitions
func (cg *ConsumerGroup) Owners() map[string]map[int32]string {
	return cg.owners
}

func (cg *ConsumerGroup) watchTopics(topics []string) (<-chan string, []*zk.Watcher, error) {
	ch := make(chan string)
	cases := make([]reflect.SelectCase, len(topics))
	watchers := make([]*zk.Watcher, len(topics))
	for i, topic := range topics {
		w, err := cg.storage.watchTopic(topic)
		if err != nil {
			return nil, nil, err
		}
		watchers[i] = w
		cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(w.EvCh)}
	}
	go func(cases []reflect.SelectCase, ch chan string, topics []string) {
		chosen, _, ok := reflect.Select(cases)
		if !ok {
			//the chosen channel has been closed.
			return
		}
		topic := topics[chosen]
		num, err := cg.storage.getPartitionsNum(topic)
		if err != nil {
			cg.logger.WithFields(logrus.Fields{
				"topic": topic,
				"err":   err,
			}).Error("Failed to get partitions in zookeeper after topic metadata change")
			return
		}
		for {
			saramaClient := cg.saramaClients[topic]
			saramaClient.RefreshMetadata(topic)
			partitions, err := saramaClient.Partitions(topic)
			if err != nil {
				cg.logger.WithFields(logrus.Fields{
					"topic": topic,
					"err":   err,
				}).Error("Failed to get partitions in broker after topic metadata change")
				return
			}
			if len(partitions) == num {
				break
			}
			time.Sleep(100 * time.Millisecond)
		}

		ch <- topics[chosen]
	}(cases, ch, topics)
	return ch, watchers, nil
}
<commit_msg>FIX: watch rebalance may blocking while the triggerCh receiver wasn't ready and close would blocking<commit_after>package consumergroup

import (
	"errors"
	"fmt"
	"reflect"
	"runtime/debug"
	"sync"
	"time"

	"github.com/Shopify/sarama"
	"github.com/meitu/go-zookeeper/zk"
	"github.com/sirupsen/logrus"
)

const (
	cgInit = iota
	cgStart
	cgStopped
)

const (
	restartEvent = 1
)

// ConsumerGroup consume message from Kafka with rebalancing supports
type ConsumerGroup struct {
	name            string
	storage         groupStorage
	topicConsumers  map[string]*topicConsumer
	saramaClients   map[string]sarama.Client
	saramaConsumers map[string]sarama.Consumer

	id          string
	state       int
	wg          sync.WaitGroup
	stopCh      chan struct{}
	triggerCh   chan int
	triggerOnce *sync.Once
	owners      map[string]map[int32]string

	config *Config
	logger *logrus.Logger

	onLoad, onClose []func()
}

// NewConsumerGroup create the ConsumerGroup instance with config
func NewConsumerGroup(config *Config) (*ConsumerGroup, error) {
	if config == nil {
		return nil, errors.New("config can't be empty")
	}
	err := config.validate()
	if err != nil {
		return nil, fmt.Errorf("vaildate config failed, as %s", err)
	}

	cg := new(ConsumerGroup)
	cg.state = cgInit
	cg.config = config
	cg.id = config.ConsumerID
	if cg.id == "" {
		cg.id = genConsumerID()
	}
	cg.name = config.GroupID
	cg.triggerCh = make(chan int)
	cg.topicConsumers = make(map[string]*topicConsumer)
	cg.saramaClients = make(map[string]sarama.Client)
	cg.saramaConsumers = make(map[string]sarama.Consumer)
	cg.onLoad = make([]func(), 0)
	cg.onClose = make([]func(), 0)
	cg.storage = newZKGroupStorage(config.ZkList, config.ZkSessionTimeout)
	cg.logger = logrus.New()
	if _, ok := cg.storage.(*zkGroupStorage); ok {
		cg.storage.(*zkGroupStorage).Chroot(config.Chroot)
	}

	err = cg.initSaramaConsumer()
	if err != nil {
		return nil, fmt.Errorf("init sarama consumer, as %s", err)
	}
	cg.owners = make(map[string]map[int32]string)
	for _, topic := range config.TopicList {
		cg.topicConsumers[topic] = newTopicConsumer(cg, topic)
		cg.owners[topic] = make(map[int32]string)
	}
	return cg, nil
}

func (cg *ConsumerGroup) initSaramaConsumer() error {
	brokerList, err := cg.storage.getBrokerList()
	if err != nil {
		return err
	}
	if len(brokerList) == 0 {
		return errors.New("no broker alive")
	}
	for _, topic := range cg.config.TopicList {
		saramaClient, err := sarama.NewClient(brokerList, cg.config.SaramaConfig)
		if err != nil {
			return err
		}
		saramaConsumer, err := sarama.NewConsumerFromClient(saramaClient)
		if err != nil {
			return err
		}
		cg.saramaClients[topic] = saramaClient
		cg.saramaConsumers[topic] = saramaConsumer
	}
	return nil
}

// Start would register ConsumerGroup, and rebalance would be triggered.
// ConsumerGroup computes the partitions which should be consumed by consumer's num, and start fetching message.
func (cg *ConsumerGroup) Start() error {
	// exit when failed to register the consumer
	err := cg.storage.registerConsumer(cg.name, cg.id, nil)
	if err != nil && err != zk.ErrNodeExists {
		return err
	}
	cg.wg.Add(1)
	go cg.start()
	return nil
}

// Stop would unregister ConsumerGroup, and rebalance would be triggered.
// The partitions which consumed by this ConsumerGroup would be assigned to others.
func (cg *ConsumerGroup) Stop() {
	cg.stop()
	cg.wg.Wait()
}

// SetLogger use to set the user's logger the consumer group
func (cg *ConsumerGroup) SetLogger(l *logrus.Logger) {
	if l != nil {
		cg.logger = l
	}
}

// IsStopped return whether the ConsumerGroup was stopped or not.
func (cg *ConsumerGroup) IsStopped() bool {
	return cg.state == cgStopped
}

func (cg *ConsumerGroup) callRecover() {
	if err := recover(); err != nil {
		cg.logger.WithFields(logrus.Fields{
			"group": cg.name,
			"err":   err,
			"stack": string(debug.Stack()),
		}).Error("Recover panic")
		cg.stop()
	}
}

func (cg *ConsumerGroup) start() {
	var wg sync.WaitGroup

	defer cg.callRecover()
	defer func() {
		cg.state = cgStopped
		err := cg.storage.deleteConsumer(cg.name, cg.id)
		if err != nil {
			cg.logger.WithFields(logrus.Fields{
				"group": cg.name,
				"err":   err,
			}).Error("Failed to delete consumer from zk")
		}
		for _, tc := range cg.topicConsumers {
			close(tc.messages)
			close(tc.errors)
		}
		cg.wg.Done()
	}()

CONSUME_TOPIC_LOOP:
	for {
		cg.logger.WithField("group", cg.name).Info("Consumer group started")
		cg.triggerOnce = new(sync.Once)
		cg.stopCh = make(chan struct{})

		err := cg.watchRebalance()
		if err != nil {
			cg.logger.WithFields(logrus.Fields{
				"group": cg.name,
				"err":   err,
			}).Error("Failed to watch rebalance")
			cg.stop()
			return
		}
		wg.Add(1)
		go func() {
			defer cg.callRecover()
			defer wg.Done()
			cg.autoReconnect(cg.storage.(*zkGroupStorage).sessionTimeout / 3)
		}()
		for _, consumer := range cg.topicConsumers {
			wg.Add(1)
			consumer.start()
			go func(tc *topicConsumer) {
				defer cg.callRecover()
				defer wg.Done()
				tc.wg.Wait()
				cg.logger.WithFields(logrus.Fields{
					"group": tc.group,
					"topic": tc.name,
				}).Info("Stop the topic consumer")
			}(consumer)
		}
		cg.state = cgStart
		for _, onLoadFunc := range cg.onLoad {
			onLoadFunc()
		}
		msg := <-cg.triggerCh
		for _, onCloseFunc := range cg.onClose {
			onCloseFunc()
		}
		switch msg {
		case restartEvent:
			close(cg.stopCh)
			// The stop channel was used to notify partition's consumer to stop consuming when rebalance is triggered.
			// So we should reinit when rebalance was triggered, as it would be closed.
			wg.Wait()
			continue CONSUME_TOPIC_LOOP
		default:
			close(cg.stopCh)
			cg.logger.WithField("group", cg.name).Info("ConsumerGroup is stopping")
			wg.Wait()
			cg.logger.WithField("group", cg.name).Info("ConsumerGroup was stopped")
			return
		}
	}
}

func (cg *ConsumerGroup) stop() {
	cg.triggerOnce.Do(func() { close(cg.triggerCh) })
}

func (cg *ConsumerGroup) triggerRebalance() {
	cg.triggerOnce.Do(func() { cg.triggerCh <- restartEvent })
}

func (cg *ConsumerGroup) getPartitionConsumer(topic string, partition int32, nextOffset int64) (sarama.PartitionConsumer, error) {
	saramaConsumer := cg.saramaConsumers[topic]
	consumer, err := saramaConsumer.ConsumePartition(topic, partition, nextOffset)
	if err == sarama.ErrOffsetOutOfRange {
		nextOffset = cg.config.OffsetAutoReset
		cg.logger.WithFields(logrus.Fields{
			"group":     cg.name,
			"topic":     topic,
			"partition": partition,
			"offset":    nextOffset,
		}).Error("Partition's offset was out of range, use auto-reset")
		consumer, err = saramaConsumer.ConsumePartition(topic, partition, nextOffset)
	}
	return consumer, err
}

// GetMessages was used to get a unbuffered message's channel from specified topic
func (cg *ConsumerGroup) GetMessages(topic string) (<-chan *sarama.ConsumerMessage, bool) {
	if topicConsumer, ok := cg.topicConsumers[topic]; ok {
		return topicConsumer.messages, true
	}
	return nil, false
}

// GetErrors was used to get a unbuffered error's channel from specified topic
func (cg *ConsumerGroup) GetErrors(topic string) (<-chan *sarama.ConsumerError, bool) {
	if topicConsumer, ok := cg.topicConsumers[topic]; ok {
		return topicConsumer.errors, true
	}
	return nil, false
}

// OnLoad load callback function that runs after startup
func (cg *ConsumerGroup) OnLoad(cb func()) {
	cg.onLoad = append(cg.onLoad, cb)
}

// OnClose load callback function that runs before the end
func (cg *ConsumerGroup) OnClose(cb func()) {
	cg.onClose = append(cg.onClose, cb)
}

func (cg *ConsumerGroup) autoReconnect(interval time.Duration) {
	timer := time.NewTimer(interval)
	cg.logger.WithField("group", cg.name).Info("The auto-reconnect consumer thread was started")
	defer cg.logger.WithField("group", cg.name).Info("The auto-reconnect consumer thread was stopped")
	for {
		select {
		case <-cg.stopCh:
			return
		case <-timer.C:
			timer.Reset(interval)
			exist, err := cg.storage.existsConsumer(cg.name, cg.id)
			if err != nil {
				cg.logger.WithFields(logrus.Fields{
					"group": cg.name,
					"err":   err,
				}).Error("Failed to check consumer existence")
				break
			}
			if exist {
				break
			}
			err = cg.storage.registerConsumer(cg.name, cg.id, nil)
			if err != nil {
				cg.logger.WithFields(logrus.Fields{
					"group": cg.name,
					"err":   err,
				}).Error("Failed to re-register consumer")
			}
		}
	}
}

func (cg *ConsumerGroup) watchRebalance() error {
	consumersWatcher, err := cg.storage.watchConsumerList(cg.name)
	if err != nil {
		return err
	}
	topicsChange, topicWatchers, err := cg.watchTopics(cg.config.TopicList)
	if err != nil {
		return err
	}
	go func() {
		defer cg.callRecover()
		cg.logger.WithField("group", cg.name).Info("Rebalance watcher thread was started")
		select {
		case <-consumersWatcher.EvCh:
			cg.triggerRebalance()
			cg.logger.WithField("group", cg.name).Info("Trigger rebalance while consumers was changed")
			for _, tw := range topicWatchers {
				cg.storage.removeWatcher(tw)
			}
		case topic := <-topicsChange:
			cg.triggerRebalance()
			cg.logger.WithFields(logrus.Fields{
				"group": cg.name,
				"topic": topic,
			}).Info("Trigger rebalance while partitions was changed")
			cg.storage.removeWatcher(consumersWatcher)
		case <-cg.stopCh:
		}
		cg.logger.WithField("group", cg.name).Info("Rebalance watcher thread was exited")
	}()
	return nil
}

// CommitOffset is used to commit offset when auto commit was disabled.
func (cg *ConsumerGroup) CommitOffset(topic string, partition int32, offset int64) error {
	if cg.config.OffsetAutoCommitEnable {
		return errors.New("commit offset take effect when offset auto commit was disabled")
	}
	return cg.storage.commitOffset(cg.name, topic, partition, offset)
}

// GetOffsets return the offset in memory for debug
func (cg *ConsumerGroup) GetOffsets() map[string]interface{} {
	topics := make(map[string]interface{})
	for topic, tc := range cg.topicConsumers {
		topics[topic] = tc.getOffsets()
	}
	return topics
}

// Owners return owners of all partitions
func (cg *ConsumerGroup) Owners() map[string]map[int32]string {
	return cg.owners
}

func (cg *ConsumerGroup) watchTopics(topics []string) (<-chan string, []*zk.Watcher, error) {
	ch := make(chan string)
	cases := make([]reflect.SelectCase, len(topics))
	watchers := make([]*zk.Watcher, len(topics))
	for i, topic := range topics {
		w, err := cg.storage.watchTopic(topic)
		if err != nil {
			return nil, nil, err
		}
		watchers[i] = w
		cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(w.EvCh)}
	}
	go func(cases []reflect.SelectCase, ch chan string, topics []string) {
		chosen, _, ok := reflect.Select(cases)
		if !ok {
			//the chosen channel has been closed.
			return
		}
		topic := topics[chosen]
		num, err := cg.storage.getPartitionsNum(topic)
		if err != nil {
			cg.logger.WithFields(logrus.Fields{
				"topic": topic,
				"err":   err,
			}).Error("Failed to get partitions in zookeeper after topic metadata change")
			return
		}
		for {
			saramaClient := cg.saramaClients[topic]
			saramaClient.RefreshMetadata(topic)
			partitions, err := saramaClient.Partitions(topic)
			if err != nil {
				cg.logger.WithFields(logrus.Fields{
					"topic": topic,
					"err":   err,
				}).Error("Failed to get partitions in broker after topic metadata change")
				return
			}
			if len(partitions) == num {
				break
			}
			time.Sleep(100 * time.Millisecond)
		}

		ch <- topics[chosen]
	}(cases, ch, topics)
	return ch, watchers, nil
}
<|endoftext|>


<commit_before>/*
Copyright 2011 Google Inc.

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 handlers

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"

	"camlistore.org/pkg/blob"
	"camlistore.org/pkg/blobserver"
	"camlistore.org/pkg/context"
)

const defaultMaxEnumerate = 10000
const defaultEnumerateSize = 100

type blobInfo struct {
	blob.Ref
	os.FileInfo
	error
}

func CreateEnumerateHandler(storage blobserver.BlobEnumerator) http.Handler {
	return http.HandlerFunc(func(conn http.ResponseWriter, req *http.Request) {
		handleEnumerateBlobs(conn, req, storage)
	})
}

const errMsgMaxWaitSecWithAfter = "Can't use 'maxwaitsec' with 'after'.\n"

func handleEnumerateBlobs(conn http.ResponseWriter, req *http.Request, storage blobserver.BlobEnumerator) {
	// Potential input parameters
	formValueLimit := req.FormValue("limit")
	formValueMaxWaitSec := req.FormValue("maxwaitsec")
	formValueAfter := req.FormValue("after")

	maxEnumerate := defaultMaxEnumerate
	if config, ok := storage.(blobserver.MaxEnumerateConfig); ok {
		maxEnumerate = config.MaxEnumerate() - 1 // Since we'll add one below.
	}

	limit := defaultEnumerateSize
	if formValueLimit != "" {
		n, err := strconv.ParseUint(formValueLimit, 10, 32)
		if err != nil || n > uint64(maxEnumerate) {
			limit = maxEnumerate
		} else {
			limit = int(n)
		}
	}

	waitSeconds := 0
	if formValueMaxWaitSec != "" {
		waitSeconds, _ = strconv.Atoi(formValueMaxWaitSec)
		if waitSeconds != 0 && formValueAfter != "" {
			conn.WriteHeader(http.StatusBadRequest)
			fmt.Fprintf(conn, errMsgMaxWaitSecWithAfter)
			return
		}
		switch {
		case waitSeconds < 0:
			waitSeconds = 0
		case waitSeconds > 30:
			// TODO: don't hard-code 30.  push this up into a blobserver interface
			// for getting the configuration of the server (ultimately a flag in
			// in the binary)
			waitSeconds = 30
		}
	}

	conn.Header().Set("Content-Type", "text/javascript; charset=utf-8")
	fmt.Fprintf(conn, "{\n  \"blobs\": [\n")

	loop := true
	needsComma := false
	deadline := time.Now().Add(time.Duration(waitSeconds) * time.Second)
	after := ""
	for loop && (waitSeconds == 0 || time.Now().After(deadline)) {
		if waitSeconds == 0 {
			loop = false
		}

		blobch := make(chan blob.SizedRef, 100)
		resultch := make(chan error, 1)
		go func() {
			resultch <- storage.EnumerateBlobs(context.TODO(), blobch, formValueAfter, limit+1)
		}()

		endsReached := 0
		gotBlobs := 0
		for endsReached < 2 {
			select {
			case sb, ok := <-blobch:
				if !ok {
					endsReached++
					if gotBlobs <= limit {
						after = ""
					}
					continue
				}
				gotBlobs++
				loop = false
				if gotBlobs > limit {
					// We requested one more from storage than the user asked for.
					// Now we know to return a "continueAfter" response key.
					// But we don't return this blob.
					continue
				}
				blobName := sb.Ref.String()
				if needsComma {
					fmt.Fprintf(conn, ",\n")
				}
				fmt.Fprintf(conn, "    {\"blobRef\": \"%s\", \"size\": %d}",
					blobName, sb.Size)
				after = blobName
				needsComma = true
			case err := <-resultch:
				if err != nil {
					log.Printf("Error during enumerate: %v", err)
					fmt.Fprintf(conn, "{{{ SERVER ERROR }}}")
					return
				}
				endsReached++
			}
		}

		if loop {
			blobserver.WaitForBlob(storage, deadline, nil)
		}
	}
	fmt.Fprintf(conn, "\n  ]")
	if after != "" {
		fmt.Fprintf(conn, ",\n  \"continueAfter\": \"%s\"", after)
	}
	const longPollSupported = true
	if longPollSupported {
		fmt.Fprintf(conn, ",\n  \"canLongPoll\": true")
	}
	fmt.Fprintf(conn, "\n}\n")
}
<commit_msg>blobserver/handler: rename conn to rw, per more recent conventions<commit_after>/*
Copyright 2011 Google Inc.

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 handlers

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"

	"camlistore.org/pkg/blob"
	"camlistore.org/pkg/blobserver"
	"camlistore.org/pkg/context"
)

const defaultMaxEnumerate = 10000
const defaultEnumerateSize = 100

type blobInfo struct {
	blob.Ref
	os.FileInfo
	error
}

func CreateEnumerateHandler(storage blobserver.BlobEnumerator) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		handleEnumerateBlobs(rw, req, storage)
	})
}

const errMsgMaxWaitSecWithAfter = "Can't use 'maxwaitsec' with 'after'.\n"

func handleEnumerateBlobs(rw http.ResponseWriter, req *http.Request, storage blobserver.BlobEnumerator) {
	// Potential input parameters
	formValueLimit := req.FormValue("limit")
	formValueMaxWaitSec := req.FormValue("maxwaitsec")
	formValueAfter := req.FormValue("after")

	maxEnumerate := defaultMaxEnumerate
	if config, ok := storage.(blobserver.MaxEnumerateConfig); ok {
		maxEnumerate = config.MaxEnumerate() - 1 // Since we'll add one below.
	}

	limit := defaultEnumerateSize
	if formValueLimit != "" {
		n, err := strconv.ParseUint(formValueLimit, 10, 32)
		if err != nil || n > uint64(maxEnumerate) {
			limit = maxEnumerate
		} else {
			limit = int(n)
		}
	}

	waitSeconds := 0
	if formValueMaxWaitSec != "" {
		waitSeconds, _ = strconv.Atoi(formValueMaxWaitSec)
		if waitSeconds != 0 && formValueAfter != "" {
			rw.WriteHeader(http.StatusBadRequest)
			fmt.Fprintf(rw, errMsgMaxWaitSecWithAfter)
			return
		}
		switch {
		case waitSeconds < 0:
			waitSeconds = 0
		case waitSeconds > 30:
			// TODO: don't hard-code 30.  push this up into a blobserver interface
			// for getting the configuration of the server (ultimately a flag in
			// in the binary)
			waitSeconds = 30
		}
	}

	rw.Header().Set("Content-Type", "text/javascript; charset=utf-8")
	fmt.Fprintf(rw, "{\n  \"blobs\": [\n")

	loop := true
	needsComma := false
	deadline := time.Now().Add(time.Duration(waitSeconds) * time.Second)
	after := ""
	for loop && (waitSeconds == 0 || time.Now().After(deadline)) {
		if waitSeconds == 0 {
			loop = false
		}

		blobch := make(chan blob.SizedRef, 100)
		resultch := make(chan error, 1)
		go func() {
			resultch <- storage.EnumerateBlobs(context.TODO(), blobch, formValueAfter, limit+1)
		}()

		endsReached := 0
		gotBlobs := 0
		for endsReached < 2 {
			select {
			case sb, ok := <-blobch:
				if !ok {
					endsReached++
					if gotBlobs <= limit {
						after = ""
					}
					continue
				}
				gotBlobs++
				loop = false
				if gotBlobs > limit {
					// We requested one more from storage than the user asked for.
					// Now we know to return a "continueAfter" response key.
					// But we don't return this blob.
					continue
				}
				blobName := sb.Ref.String()
				if needsComma {
					fmt.Fprintf(rw, ",\n")
				}
				fmt.Fprintf(rw, "    {\"blobRef\": \"%s\", \"size\": %d}",
					blobName, sb.Size)
				after = blobName
				needsComma = true
			case err := <-resultch:
				if err != nil {
					log.Printf("Error during enumerate: %v", err)
					fmt.Fprintf(rw, "{{{ SERVER ERROR }}}")
					return
				}
				endsReached++
			}
		}

		if loop {
			blobserver.WaitForBlob(storage, deadline, nil)
		}
	}
	fmt.Fprintf(rw, "\n  ]")
	if after != "" {
		fmt.Fprintf(rw, ",\n  \"continueAfter\": \"%s\"", after)
	}
	const longPollSupported = true
	if longPollSupported {
		fmt.Fprintf(rw, ",\n  \"canLongPoll\": true")
	}
	fmt.Fprintf(rw, "\n}\n")
}
<|endoftext|>


<commit_before>/*
Copyright 2014 The Kubernetes Authors All rights reserved.

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 vagrant_cloud

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	neturl "net/url"
	"sort"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
)

const ProviderName = "vagrant"

// VagrantCloud is an implementation of Interface, TCPLoadBalancer and Instances for developer managed Vagrant cluster.
type VagrantCloud struct {
	saltURL  string
	saltUser string
	saltPass string
	saltAuth string
}

func init() {
	cloudprovider.RegisterCloudProvider(ProviderName, func(config io.Reader) (cloudprovider.Interface, error) { return newVagrantCloud() })
}

// SaltToken is an authorization token required by Salt REST API.
type SaltToken struct {
	Token string `json:"token"`
	User  string `json:"user"`
	EAuth string `json:"eauth"`
}

// SaltLoginResponse is the response object for a /login operation against Salt REST API.
type SaltLoginResponse struct {
	Data []SaltToken `json:"return"`
}

// SaltMinion is a machine managed by the Salt service.
type SaltMinion struct {
	Roles []string `json:"roles"`
	IP    string   `json:"node_ip"`
	Host  string   `json:"host"`
}

// SaltMinions is a map of minion name to machine information.
type SaltMinions map[string]SaltMinion

// SaltMinionsResponse is the response object for a /minions operation against Salt REST API
type SaltMinionsResponse struct {
	Minions []SaltMinions `json:"return"`
}

// newVagrantCloud creates a new instance of VagrantCloud configured to talk to the Salt REST API.
func newVagrantCloud() (*VagrantCloud, error) {
	return &VagrantCloud{
		saltURL:  "http://kubernetes-master:8000",
		saltUser: "vagrant",
		saltPass: "vagrant",
		saltAuth: "pam",
	}, nil
}

func (v *VagrantCloud) Clusters() (cloudprovider.Clusters, bool) {
	return nil, false
}

// ProviderName returns the cloud provider ID.
func (v *VagrantCloud) ProviderName() string {
	return ProviderName
}

// TCPLoadBalancer returns an implementation of TCPLoadBalancer for Vagrant cloud.
func (v *VagrantCloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) {
	return nil, false
}

// Instances returns an implementation of Instances for Vagrant cloud.
func (v *VagrantCloud) Instances() (cloudprovider.Instances, bool) {
	return v, true
}

// Zones returns an implementation of Zones for Vagrant cloud.
func (v *VagrantCloud) Zones() (cloudprovider.Zones, bool) {
	return nil, false
}

// Routes returns an implementation of Routes for Vagrant cloud.
func (v *VagrantCloud) Routes() (cloudprovider.Routes, bool) {
	return nil, false
}

// getInstanceByAddress retuns
func (v *VagrantCloud) getInstanceByAddress(address string) (*SaltMinion, error) {
	token, err := v.saltLogin()
	if err != nil {
		return nil, err
	}
	minions, err := v.saltMinions(token)
	if err != nil {
		return nil, err
	}
	filteredMinions := v.saltMinionsByRole(minions, "kubernetes-pool")
	for _, minion := range filteredMinions {
		// Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time
		if minion.IP == address {
			return &minion, nil
		}
	}
	return nil, fmt.Errorf("unable to find instance for address: %s", address)
}

// NodeAddresses returns the NodeAddresses of a particular machine instance.
func (v *VagrantCloud) NodeAddresses(instance string) ([]api.NodeAddress, error) {
	// Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time
	minion, err := v.getInstanceByAddress(instance)
	if err != nil {
		return nil, err
	}
	ip := net.ParseIP(minion.IP)
	return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: ip.String()}}, nil
}

// ExternalID returns the cloud provider ID of the specified instance (deprecated).
func (v *VagrantCloud) ExternalID(instance string) (string, error) {
	// Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time
	minion, err := v.getInstanceByAddress(instance)
	if err != nil {
		return "", err
	}
	return minion.IP, nil
}

// InstanceID returns the cloud provider ID of the specified instance.
func (v *VagrantCloud) InstanceID(instance string) (string, error) {
	return "", nil
}

// saltMinionsByRole filters a list of minions that have a matching role.
func (v *VagrantCloud) saltMinionsByRole(minions []SaltMinion, role string) []SaltMinion {
	var filteredMinions []SaltMinion
	for _, value := range minions {
		sort.Strings(value.Roles)
		if pos := sort.SearchStrings(value.Roles, role); pos < len(value.Roles) {
			filteredMinions = append(filteredMinions, value)
		}
	}
	return filteredMinions
}

// saltMinions invokes the Salt API for minions using provided token.
func (v *VagrantCloud) saltMinions(token SaltToken) ([]SaltMinion, error) {
	var minions []SaltMinion

	url := v.saltURL + "/minions"
	req, err := http.NewRequest("GET", url, nil)
	req.Header.Add("X-Auth-Token", token.Token)

	client := &http.Client{}
	resp, err := client.Do(req)

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return minions, err
	}

	var minionsResp SaltMinionsResponse
	if err = json.Unmarshal(body, &minionsResp); err != nil {
		return minions, err
	}

	for _, value := range minionsResp.Minions[0] {
		minions = append(minions, value)
	}

	return minions, nil
}

// saltLogin invokes the Salt API to get an authorization token.
func (v *VagrantCloud) saltLogin() (SaltToken, error) {
	url := v.saltURL + "/login"
	data := neturl.Values{
		"username": {v.saltUser},
		"password": {v.saltPass},
		"eauth":    {v.saltAuth},
	}

	var token SaltToken
	resp, err := http.PostForm(url, data)
	if err != nil {
		return token, err
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return token, err
	}

	var loginResp SaltLoginResponse
	if err := json.Unmarshal(body, &loginResp); err != nil {
		return token, err
	}

	if len(loginResp.Data) == 0 {
		return token, errors.New("No token found in response")
	}

	return loginResp.Data[0], nil
}

// List enumerates the set of minions instances known by the cloud provider.
func (v *VagrantCloud) List(filter string) ([]string, error) {
	token, err := v.saltLogin()
	if err != nil {
		return nil, err
	}

	minions, err := v.saltMinions(token)
	if err != nil {
		return nil, err
	}

	filteredMinions := v.saltMinionsByRole(minions, "kubernetes-pool")
	var instances []string
	for _, instance := range filteredMinions {
		// With no dedicated DNS setup in cluster, IP address is used as hostname
		instances = append(instances, instance.IP)
	}

	return instances, nil
}

func (v *VagrantCloud) GetNodeResources(name string) (*api.NodeResources, error) {
	return nil, nil
}
<commit_msg>cloudprovider: vagrant InstanceID implementation<commit_after>/*
Copyright 2014 The Kubernetes Authors All rights reserved.

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 vagrant_cloud

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	neturl "net/url"
	"sort"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
)

const ProviderName = "vagrant"

// VagrantCloud is an implementation of Interface, TCPLoadBalancer and Instances for developer managed Vagrant cluster.
type VagrantCloud struct {
	saltURL  string
	saltUser string
	saltPass string
	saltAuth string
}

func init() {
	cloudprovider.RegisterCloudProvider(ProviderName, func(config io.Reader) (cloudprovider.Interface, error) { return newVagrantCloud() })
}

// SaltToken is an authorization token required by Salt REST API.
type SaltToken struct {
	Token string `json:"token"`
	User  string `json:"user"`
	EAuth string `json:"eauth"`
}

// SaltLoginResponse is the response object for a /login operation against Salt REST API.
type SaltLoginResponse struct {
	Data []SaltToken `json:"return"`
}

// SaltMinion is a machine managed by the Salt service.
type SaltMinion struct {
	Roles []string `json:"roles"`
	IP    string   `json:"node_ip"`
	Host  string   `json:"host"`
}

// SaltMinions is a map of minion name to machine information.
type SaltMinions map[string]SaltMinion

// SaltMinionsResponse is the response object for a /minions operation against Salt REST API
type SaltMinionsResponse struct {
	Minions []SaltMinions `json:"return"`
}

// newVagrantCloud creates a new instance of VagrantCloud configured to talk to the Salt REST API.
func newVagrantCloud() (*VagrantCloud, error) {
	return &VagrantCloud{
		saltURL:  "http://kubernetes-master:8000",
		saltUser: "vagrant",
		saltPass: "vagrant",
		saltAuth: "pam",
	}, nil
}

func (v *VagrantCloud) Clusters() (cloudprovider.Clusters, bool) {
	return nil, false
}

// ProviderName returns the cloud provider ID.
func (v *VagrantCloud) ProviderName() string {
	return ProviderName
}

// TCPLoadBalancer returns an implementation of TCPLoadBalancer for Vagrant cloud.
func (v *VagrantCloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) {
	return nil, false
}

// Instances returns an implementation of Instances for Vagrant cloud.
func (v *VagrantCloud) Instances() (cloudprovider.Instances, bool) {
	return v, true
}

// Zones returns an implementation of Zones for Vagrant cloud.
func (v *VagrantCloud) Zones() (cloudprovider.Zones, bool) {
	return nil, false
}

// Routes returns an implementation of Routes for Vagrant cloud.
func (v *VagrantCloud) Routes() (cloudprovider.Routes, bool) {
	return nil, false
}

// getInstanceByAddress retuns
func (v *VagrantCloud) getInstanceByAddress(address string) (*SaltMinion, error) {
	token, err := v.saltLogin()
	if err != nil {
		return nil, err
	}
	minions, err := v.saltMinions(token)
	if err != nil {
		return nil, err
	}
	filteredMinions := v.saltMinionsByRole(minions, "kubernetes-pool")
	for _, minion := range filteredMinions {
		// Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time
		if minion.IP == address {
			return &minion, nil
		}
	}
	return nil, fmt.Errorf("unable to find instance for address: %s", address)
}

// NodeAddresses returns the NodeAddresses of a particular machine instance.
func (v *VagrantCloud) NodeAddresses(instance string) ([]api.NodeAddress, error) {
	// Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time
	minion, err := v.getInstanceByAddress(instance)
	if err != nil {
		return nil, err
	}
	ip := net.ParseIP(minion.IP)
	return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: ip.String()}}, nil
}

// ExternalID returns the cloud provider ID of the specified instance (deprecated).
func (v *VagrantCloud) ExternalID(instance string) (string, error) {
	// Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time
	minion, err := v.getInstanceByAddress(instance)
	if err != nil {
		return "", err
	}
	return minion.IP, nil
}

// InstanceID returns the cloud provider ID of the specified instance.
func (v *VagrantCloud) InstanceID(instance string) (string, error) {
	minion, err := v.getInstanceByAddress(instance)
	if err != nil {
		return "", err
	}
	return minion.IP, nil
}

// saltMinionsByRole filters a list of minions that have a matching role.
func (v *VagrantCloud) saltMinionsByRole(minions []SaltMinion, role string) []SaltMinion {
	var filteredMinions []SaltMinion
	for _, value := range minions {
		sort.Strings(value.Roles)
		if pos := sort.SearchStrings(value.Roles, role); pos < len(value.Roles) {
			filteredMinions = append(filteredMinions, value)
		}
	}
	return filteredMinions
}

// saltMinions invokes the Salt API for minions using provided token.
func (v *VagrantCloud) saltMinions(token SaltToken) ([]SaltMinion, error) {
	var minions []SaltMinion

	url := v.saltURL + "/minions"
	req, err := http.NewRequest("GET", url, nil)
	req.Header.Add("X-Auth-Token", token.Token)

	client := &http.Client{}
	resp, err := client.Do(req)

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return minions, err
	}

	var minionsResp SaltMinionsResponse
	if err = json.Unmarshal(body, &minionsResp); err != nil {
		return minions, err
	}

	for _, value := range minionsResp.Minions[0] {
		minions = append(minions, value)
	}

	return minions, nil
}

// saltLogin invokes the Salt API to get an authorization token.
func (v *VagrantCloud) saltLogin() (SaltToken, error) {
	url := v.saltURL + "/login"
	data := neturl.Values{
		"username": {v.saltUser},
		"password": {v.saltPass},
		"eauth":    {v.saltAuth},
	}

	var token SaltToken
	resp, err := http.PostForm(url, data)
	if err != nil {
		return token, err
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return token, err
	}

	var loginResp SaltLoginResponse
	if err := json.Unmarshal(body, &loginResp); err != nil {
		return token, err
	}

	if len(loginResp.Data) == 0 {
		return token, errors.New("No token found in response")
	}

	return loginResp.Data[0], nil
}

// List enumerates the set of minions instances known by the cloud provider.
func (v *VagrantCloud) List(filter string) ([]string, error) {
	token, err := v.saltLogin()
	if err != nil {
		return nil, err
	}

	minions, err := v.saltMinions(token)
	if err != nil {
		return nil, err
	}

	filteredMinions := v.saltMinionsByRole(minions, "kubernetes-pool")
	var instances []string
	for _, instance := range filteredMinions {
		// With no dedicated DNS setup in cluster, IP address is used as hostname
		instances = append(instances, instance.IP)
	}

	return instances, nil
}

func (v *VagrantCloud) GetNodeResources(name string) (*api.NodeResources, error) {
	return nil, nil
}
<|endoftext|>


<commit_before>/*
Package eventstore provides memory implementation of domain event store
*/
package eventstore

import (
	"context"
	"sync"

	"github.com/google/uuid"

	"github.com/vardius/go-api-boilerplate/pkg/domain"
	baseeventstore "github.com/vardius/go-api-boilerplate/pkg/eventstore"
)

type eventStore struct {
	sync.RWMutex
	events map[string]domain.Event
}

func (s *eventStore) Store(ctx context.Context, events []domain.Event) error {
	if len(events) == 0 {
		return nil
	}

	s.Lock()
	defer s.Unlock()

	// @TODO: check event version
	for _, e := range events {
		s.events[e.ID.String()] = e
	}

	return nil
}

func (s *eventStore) Get(ctx context.Context, id uuid.UUID) (domain.Event, error) {
	s.RLock()
	defer s.RUnlock()
	if val, ok := s.events[id.String()]; ok {
		return val, nil
	}

	return domain.NullEvent, baseeventstore.ErrEventNotFound
}

func (s *eventStore) FindAll(ctx context.Context) ([]domain.Event, error) {
	s.RLock()
	defer s.RUnlock()
	es := make([]domain.Event, 0, len(s.events))
	for _, val := range s.events {
		es = append(es, val)
	}
	return es, nil
}

func (s *eventStore) GetStream(ctx context.Context, streamID uuid.UUID, streamName string) ([]domain.Event, error) {
	s.RLock()
	defer s.RUnlock()
	e := make([]domain.Event, 0, 0)
	for _, val := range s.events {
		if val.StreamName == streamName && val.StreamID == streamID {
			e = append(e, val)
		}
	}
	return e, nil
}

// New creates in memory event store
func New() baseeventstore.EventStore {
	return &eventStore{
		events: make(map[string]domain.Event),
	}
}
<commit_msg>Sort events<commit_after>/*
Package eventstore provides memory implementation of domain event store
*/
package eventstore

import (
	"context"
	"sort"
	"sync"

	"github.com/google/uuid"

	"github.com/vardius/go-api-boilerplate/pkg/domain"
	baseeventstore "github.com/vardius/go-api-boilerplate/pkg/eventstore"
)

type eventStore struct {
	sync.RWMutex
	events map[string]domain.Event
}

func (s *eventStore) Store(ctx context.Context, events []domain.Event) error {
	if len(events) == 0 {
		return nil
	}

	s.Lock()
	defer s.Unlock()

	// @TODO: check event version
	for _, e := range events {
		s.events[e.ID.String()] = e
	}

	return nil
}

func (s *eventStore) Get(ctx context.Context, id uuid.UUID) (domain.Event, error) {
	s.RLock()
	defer s.RUnlock()
	if val, ok := s.events[id.String()]; ok {
		return val, nil
	}

	return domain.NullEvent, baseeventstore.ErrEventNotFound
}

func (s *eventStore) FindAll(ctx context.Context) ([]domain.Event, error) {
	s.RLock()
	defer s.RUnlock()
	es := make([]domain.Event, 0, len(s.events))
	for _, val := range s.events {
		es = append(es, val)
	}
	sort.SliceStable(es, func(i, j int) bool {
		return es[i].OccurredAt.Before(es[j].OccurredAt)
	})
	return es, nil
}

func (s *eventStore) GetStream(ctx context.Context, streamID uuid.UUID, streamName string) ([]domain.Event, error) {
	s.RLock()
	defer s.RUnlock()
	e := make([]domain.Event, 0, 0)
	for _, val := range s.events {
		if val.StreamName == streamName && val.StreamID == streamID {
			e = append(e, val)
		}
	}
	sort.SliceStable(e, func(i, j int) bool {
		return e[i].OccurredAt.Before(e[j].OccurredAt)
	})
	return e, nil
}

// New creates in memory event store
func New() baseeventstore.EventStore {
	return &eventStore{
		events: make(map[string]domain.Event),
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2020 The Rook Authors. All rights reserved.

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 cluster to manage a Ceph cluster.
package cluster

import (
	"context"

	cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
	"github.com/rook/rook/pkg/clusterd"
	cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
	discoverDaemon "github.com/rook/rook/pkg/daemon/discover"
	"github.com/rook/rook/pkg/operator/k8sutil"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// clientCluster struct contains a client to interact with Kubernetes object
// as well as the NamespacedName (used in requests)
type clientCluster struct {
	client    client.Client
	namespace string
	context   *clusterd.Context
}

func newClientCluster(client client.Client, namespace string, context *clusterd.Context) *clientCluster {
	return &clientCluster{
		client:    client,
		namespace: namespace,
		context:   context,
	}
}

// onK8sNodeAdd is triggered when a node is added in the Kubernetes cluster
// func (c *clientCluster) onK8sNodeAdd(object runtime.Object) bool {
func (c *clientCluster) onK8sNode(object runtime.Object) bool {
	node, ok := object.(*v1.Node)
	if !ok {
		return false
	}

	// Get CephCluster
	cluster := c.getCephCluster()

	if k8sutil.GetNodeSchedulable(*node) == false {
		logger.Debugf("node watcher: skipping cluster update. added node %q is unschedulable", node.Labels[v1.LabelHostname])
		return false
	}

	if k8sutil.NodeIsTolerable(*node, cephv1.GetOSDPlacement(cluster.Spec.Placement).Tolerations, false) == false {
		logger.Debugf("node watcher: node since it is not tolerable for cluster %q, skipping", cluster.Namespace)
		return false
	}

	if cluster.Spec.Storage.UseAllNodes == false {
		logger.Debugf("node watcher: do not use all Nodes in cluster %q, skipping", cluster.Namespace)
		return false
	}

	// Too strict? this replaces clusterInfo == nil
	if cluster.Status.Phase != cephv1.ConditionReady {
		logger.Debugf("node watcher: cluster %q is not ready. skipping orchestration", cluster.Namespace)
		return false
	}

	valid, _ := k8sutil.ValidNode(*node, cluster.Spec.Placement.All())
	if valid {
		nodeName := node.Name
		hostname, ok := node.Labels[v1.LabelHostname]
		if ok && hostname != "" {
			nodeName = hostname
		}

		// Make sure we can call Ceph properly
		// Is the node in the CRUSH map already?
		// If so we don't need to reconcile, this is done to avoid double reconcile on operator restart
		// Assume the admin key since we are watching for node status to create OSDs
		clusterInfo := cephclient.AdminClusterInfo(cluster.Namespace)
		osds, err := cephclient.GetOSDOnHost(c.context, clusterInfo, nodeName)
		if err != nil {
			// If it fails, this might be due to the the operator just starting and catching an add event for that node
			logger.Debugf("failed to get osds on node %q", nodeName)
			return false
		}

		// If they are OSDs in the CRUSH map and if the host exists in the CRUSH map, don't reconcile
		if osds != "" {
			// This is Debug level because the node receives frequent updates and this will polute the logs
			logger.Debugf("node watcher: node %q is already an OSD node with %q", nodeName, osds)
		} else {
			logger.Infof("node watcher: adding node %q to cluster %q", node.Labels[v1.LabelHostname], cluster.Namespace)
			return true
		}
	}

	return false
}

// onDeviceCMUpdate is trigger when the hot plug config map is updated
func (c *clientCluster) onDeviceCMUpdate(oldObj, newObj runtime.Object) bool {
	oldCm, ok := oldObj.(*v1.ConfigMap)
	if !ok {
		return false
	}
	logger.Debugf("hot-plug cm watcher: onDeviceCMUpdate old device cm: %+v", oldCm)

	newCm, ok := newObj.(*v1.ConfigMap)
	if !ok {
		return false
	}
	logger.Debugf("hot-plug cm watcher: onDeviceCMUpdate new device cm: %+v", newCm)

	oldDevStr, ok := oldCm.Data[discoverDaemon.LocalDiskCMData]
	if !ok {
		logger.Warning("hot-plug cm watcher: unexpected old configmap data")
		return false
	}

	newDevStr, ok := newCm.Data[discoverDaemon.LocalDiskCMData]
	if !ok {
		logger.Warning("hot-plug cm watcher: unexpected new configmap data")
		return false
	}

	devicesEqual, err := discoverDaemon.DeviceListsEqual(oldDevStr, newDevStr)
	if err != nil {
		logger.Warningf("hot-plug cm watcher: failed to compare device lists. %v", err)
		return false
	}

	if devicesEqual {
		logger.Debug("hot-plug cm watcher: device lists are equal. skipping orchestration")
		return false
	}

	// Get CephCluster
	cluster := c.getCephCluster()

	if cluster.Status.Phase != cephv1.ConditionReady {
		logger.Debugf("hot-plug cm watcher: cluster %q is not ready. skipping orchestration.", cluster.Namespace)
		return false
	}

	if len(cluster.Spec.Storage.StorageClassDeviceSets) > 0 {
		logger.Info("hot-plug cm watcher: skip orchestration on device config map update for OSDs on PVC")
		return false
	}

	logger.Infof("hot-plug cm watcher: running orchestration for namespace %q after device change", cluster.Namespace)
	return true
}

func (c *clientCluster) getCephCluster() *cephv1.CephCluster {
	clusterList := &cephv1.CephClusterList{}

	err := c.client.List(context.TODO(), clusterList, client.InNamespace(c.namespace))
	if err != nil {
		logger.Debugf("%q: failed to fetch CephCluster %v", controllerName, err)
		return &cephv1.CephCluster{}
	}
	if len(clusterList.Items) == 0 {
		logger.Debugf("%q: no CephCluster resource found in namespace %q", controllerName, c.namespace)
		return &cephv1.CephCluster{}
	}

	return &clusterList.Items[0]
}
<commit_msg>ceph: operator should only "watch" OSD placement nodes<commit_after>/*
Copyright 2020 The Rook Authors. All rights reserved.

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 cluster to manage a Ceph cluster.
package cluster

import (
	"context"

	cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
	"github.com/rook/rook/pkg/clusterd"
	cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
	discoverDaemon "github.com/rook/rook/pkg/daemon/discover"
	"github.com/rook/rook/pkg/operator/k8sutil"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// clientCluster struct contains a client to interact with Kubernetes object
// as well as the NamespacedName (used in requests)
type clientCluster struct {
	client    client.Client
	namespace string
	context   *clusterd.Context
}

func newClientCluster(client client.Client, namespace string, context *clusterd.Context) *clientCluster {
	return &clientCluster{
		client:    client,
		namespace: namespace,
		context:   context,
	}
}

// onK8sNodeAdd is triggered when a node is added in the Kubernetes cluster
// func (c *clientCluster) onK8sNodeAdd(object runtime.Object) bool {
func (c *clientCluster) onK8sNode(object runtime.Object) bool {
	node, ok := object.(*v1.Node)
	if !ok {
		return false
	}

	// Get CephCluster
	cluster := c.getCephCluster()

	if k8sutil.GetNodeSchedulable(*node) == false {
		logger.Debugf("node watcher: skipping cluster update. added node %q is unschedulable", node.Labels[v1.LabelHostname])
		return false
	}

	if k8sutil.NodeIsTolerable(*node, cephv1.GetOSDPlacement(cluster.Spec.Placement).Tolerations, false) == false {
		logger.Debugf("node watcher: node since it is not tolerable for cluster %q, skipping", cluster.Namespace)
		return false
	}

	if cluster.Spec.Storage.UseAllNodes == false {
		logger.Debugf("node watcher: do not use all Nodes in cluster %q, skipping", cluster.Namespace)
		return false
	}

	// Too strict? this replaces clusterInfo == nil
	if cluster.Status.Phase != cephv1.ConditionReady {
		logger.Debugf("node watcher: cluster %q is not ready. skipping orchestration", cluster.Namespace)
		return false
	}

	valid, _ := k8sutil.ValidNode(*node, cephv1.GetOSDPlacement(cluster.Spec.Placement))
	if valid {
		nodeName := node.Name
		hostname, ok := node.Labels[v1.LabelHostname]
		if ok && hostname != "" {
			nodeName = hostname
		}

		// Make sure we can call Ceph properly
		// Is the node in the CRUSH map already?
		// If so we don't need to reconcile, this is done to avoid double reconcile on operator restart
		// Assume the admin key since we are watching for node status to create OSDs
		clusterInfo := cephclient.AdminClusterInfo(cluster.Namespace)
		osds, err := cephclient.GetOSDOnHost(c.context, clusterInfo, nodeName)
		if err != nil {
			// If it fails, this might be due to the the operator just starting and catching an add event for that node
			logger.Debugf("failed to get osds on node %q", nodeName)
			return false
		}

		// If they are OSDs in the CRUSH map and if the host exists in the CRUSH map, don't reconcile
		if osds != "" {
			// This is Debug level because the node receives frequent updates and this will polute the logs
			logger.Debugf("node watcher: node %q is already an OSD node with %q", nodeName, osds)
		} else {
			logger.Infof("node watcher: adding node %q to cluster %q", node.Labels[v1.LabelHostname], cluster.Namespace)
			return true
		}
	}

	return false
}

// onDeviceCMUpdate is trigger when the hot plug config map is updated
func (c *clientCluster) onDeviceCMUpdate(oldObj, newObj runtime.Object) bool {
	oldCm, ok := oldObj.(*v1.ConfigMap)
	if !ok {
		return false
	}
	logger.Debugf("hot-plug cm watcher: onDeviceCMUpdate old device cm: %+v", oldCm)

	newCm, ok := newObj.(*v1.ConfigMap)
	if !ok {
		return false
	}
	logger.Debugf("hot-plug cm watcher: onDeviceCMUpdate new device cm: %+v", newCm)

	oldDevStr, ok := oldCm.Data[discoverDaemon.LocalDiskCMData]
	if !ok {
		logger.Warning("hot-plug cm watcher: unexpected old configmap data")
		return false
	}

	newDevStr, ok := newCm.Data[discoverDaemon.LocalDiskCMData]
	if !ok {
		logger.Warning("hot-plug cm watcher: unexpected new configmap data")
		return false
	}

	devicesEqual, err := discoverDaemon.DeviceListsEqual(oldDevStr, newDevStr)
	if err != nil {
		logger.Warningf("hot-plug cm watcher: failed to compare device lists. %v", err)
		return false
	}

	if devicesEqual {
		logger.Debug("hot-plug cm watcher: device lists are equal. skipping orchestration")
		return false
	}

	// Get CephCluster
	cluster := c.getCephCluster()

	if cluster.Status.Phase != cephv1.ConditionReady {
		logger.Debugf("hot-plug cm watcher: cluster %q is not ready. skipping orchestration.", cluster.Namespace)
		return false
	}

	if len(cluster.Spec.Storage.StorageClassDeviceSets) > 0 {
		logger.Info("hot-plug cm watcher: skip orchestration on device config map update for OSDs on PVC")
		return false
	}

	logger.Infof("hot-plug cm watcher: running orchestration for namespace %q after device change", cluster.Namespace)
	return true
}

func (c *clientCluster) getCephCluster() *cephv1.CephCluster {
	clusterList := &cephv1.CephClusterList{}

	err := c.client.List(context.TODO(), clusterList, client.InNamespace(c.namespace))
	if err != nil {
		logger.Debugf("%q: failed to fetch CephCluster %v", controllerName, err)
		return &cephv1.CephCluster{}
	}
	if len(clusterList.Items) == 0 {
		logger.Debugf("%q: no CephCluster resource found in namespace %q", controllerName, c.namespace)
		return &cephv1.CephCluster{}
	}

	return &clusterList.Items[0]
}
<|endoftext|>