diff --git a/memcache/memcache.go b/memcache/memcache.go index 545a3e79..12591fb9 100644 --- a/memcache/memcache.go +++ b/memcache/memcache.go @@ -48,7 +48,7 @@ var ( // CompareAndSwap) failed because the condition was not satisfied. ErrNotStored = errors.New("memcache: item not stored") - // ErrServer means that a server error occurred. + // ErrServerError means that a server error occurred. ErrServerError = errors.New("memcache: server error") // ErrNoStats means that no statistics were available. @@ -61,6 +61,10 @@ var ( // ErrNoServers is returned when no servers are configured or available. ErrNoServers = errors.New("memcache: no servers configured or available") + + // ErrDumpDisable is returned when the "stats cachedump" and + // "lru_crawler metadump" commands are disabled. + ErrDumpDisable = errors.New("memcache: cachedump/metadump disabled") ) const ( @@ -101,6 +105,7 @@ func legalKey(key string) bool { var ( crlf = []byte("\r\n") space = []byte(" ") + colon = []byte(":") resultOK = []byte("OK\r\n") resultStored = []byte("STORED\r\n") resultNotStored = []byte("NOT_STORED\r\n") @@ -308,6 +313,10 @@ func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) e return nil } +// FlushAll Invalidates all existing cache items. +// +// This command does not pause the server, as it returns immediately. +// It does not free up or flush memory at all, it just causes all items to expire. func (c *Client) FlushAll() error { return c.selector.Each(c.flushAllFromAddr) } diff --git a/memcache/memcache_test.go b/memcache/memcache_test.go index 70d47026..1166af85 100644 --- a/memcache/memcache_test.go +++ b/memcache/memcache_test.go @@ -201,6 +201,41 @@ func testWithClient(t *testing.T, c *Client) { } testTouchWithClient(t, c) + // Test stats + if stats, err := c.StatsServers(); err != nil { + t.Errorf("Stats error: %v", err) + } else { + for addr, s := range stats { + if s.Errs != nil { + t.Errorf("Stats server %s errors: %v", addr.String(), s.Errs) + } else if s.ServerErr != nil { + t.Errorf("Stats server error: %s %v", addr.String(), s.ServerErr) + + } + } + } + // Test stats item + if stats, err := c.StatsItemsServers(0); err != nil { + t.Errorf("Stats error: %v", err) + } else { + for addr, s := range stats { + if s.ServerErr != nil { + t.Errorf("Stats server %s error: %v", addr.String(), s.ServerErr) + } + } + } + + // Test stats item + if stats, err := c.StatsSlabsServers(); err != nil { + t.Errorf("Stats error: %v", err) + } else { + for addr, s := range stats { + if s.ServerErr != nil { + t.Errorf("Stats server %s error: %v", addr.String(), s.ServerErr) + } + } + } + // Test Delete All err = c.DeleteAll() checkErr(err, "DeleteAll: %v", err) @@ -209,6 +244,25 @@ func testWithClient(t *testing.T, c *Client) { t.Errorf("post-DeleteAll want ErrCacheMiss, got %v", err) } + + //Sleep to allow the memcache server to refresh + time.Sleep(time.Second * 5) + if stats, err := c.StatsItemsServers(0); err != nil { + t.Errorf("Stats error: %v", err) + } else { + for addr, stats := range stats { + if len(stats.StatsItemsSlabs) != 0 { + keys := []string{} + for _, itemsStats := range stats.StatsItemsSlabs { + for _, k := range itemsStats.Keys { + keys = append(keys, k.Key) + } + } + t.Errorf("post-DeleteAll stats items in server %s has %d item(s), key(s): %v", addr.String(), len(stats.StatsItemsSlabs), keys) + } + } + } + // Test Ping err = c.Ping() checkErr(err, "error ping: %s", err) diff --git a/memcache/stats.go b/memcache/stats.go new file mode 100644 index 00000000..c354fc12 --- /dev/null +++ b/memcache/stats.go @@ -0,0 +1,378 @@ +/* +Copyright 2019 gomemcache Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package memcache + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "net" + "reflect" + "strconv" + "strings" + "sync" + "unicode" +) + +type errorsSlice []error + +var errUnknownStat = errors.New("unknown stat") + +func (errs errorsSlice) Error() string { + switch len(errs) { + case 0: + return "" + case 1: + return errs[0].Error() + } + n := (len(errs) - 1) + for i := 0; i < len(errs); i++ { + n += len(errs[i].Error()) + } + + var b strings.Builder + b.Grow(n) + b.WriteString(errs[0].Error()) + for _, err := range errs[1:] { + b.WriteByte(':') + b.WriteString(err.Error()) + } + return b.String() +} + +func (errs *errorsSlice) AppendError(err error) { + if err == nil { + return + } + if errs == nil { + errs = &errorsSlice{err} + return + } + set := *errs + set = append(set, err) + *errs = set +} + +// ServerStats contains the general statistics from one server. +type ServerStats struct { + // ServerErr error if can't get the stats information. + ServerErr error + // Errs contains the errors that occurred while parsing the response. + Errs errorsSlice + // UnknownStats contains the stats that are not in the struct. This can be useful if the memcached developers add new stats in newer versions. + UnknownStats map[string]string + + // Version version string of this server. + Version string + // AcceptingConns whether or not server is accepting conns. + AcceptingConns bool + // HashIsExpanding indicates if the hash table is being grown to a new size. + HashIsExpanding bool + // SlabReassignRunning if a slab page is being moved. + SlabReassignRunning bool + // Pid process id of this server process. + Pid uint32 + // Uptime number of secs since the server started. + Uptime uint32 + // Time current UNIX time according to the server. + Time uint32 + // RusageUser accumulated user time for this process (seconds:microseconds). + RusageUser float64 + // RusageSystem accumulated system time for this process (seconds:microseconds). + RusageSystem float64 + // MaxConnections max number of simultaneous connections. + MaxConnections uint32 + // CurrConnections number of open connections. + CurrConnections uint32 + // TotalConnections total number of connections opened since the server started running. + TotalConnections uint32 + // ConnectionStructures number of connection structures allocated by the server. + ConnectionStructures uint32 + // ReservedFds number of misc fds used internally. + ReservedFds uint32 + // Threads number of worker threads requested. (see doc/threads.txt). + Threads uint32 + // HashPowerLevel current size multiplier for hash table. + HashPowerLevel uint32 + // SlabGlobalPagePool slab pages returned to global pool for reassignment to other slab classes. + SlabGlobalPagePool uint32 + // PointerSize default size of pointers on the host OS (generally 32 or 64). + PointerSize uint64 + // CurrItems current number of items stored. + CurrItems uint64 + // TotalItems total number of items stored since the server started. + TotalItems uint64 + // Bytes current number of bytes used to store items. + Bytes uint64 + // RejectedConnections conns rejected in maxconns_fast mode. + RejectedConnections uint64 + // CmdGet cumulative number of retrieval reqs. + CmdGet uint64 + // CmdSet cumulative number of storage reqs. + CmdSet uint64 + // CmdFlush cumulative number of flush reqs. + CmdFlush uint64 + // CmdTouch cumulative number of touch reqs. + CmdTouch uint64 + // GetHits number of keys that have been requested and found present. + GetHits uint64 + // GetMisses number of items that have been requested and not found. + GetMisses uint64 + // GetExpired number of items that have been requested but had already expired. + GetExpired uint64 + // GetFlushed number of items that have been requested but have been flushed via flush_all. + GetFlushed uint64 + // DeleteMisses number of deletions reqs for missing keys. + DeleteMisses uint64 + // DeleteHits number of deletion reqs resulting in an item being removed. + DeleteHits uint64 + // IncrMisses number of incr reqs against missing keys. + IncrMisses uint64 + // IncrHits number of successful incr reqs. + IncrHits uint64 + // DecrMisses number of decr reqs against missing keys. + DecrMisses uint64 + // DecrHits number of successful decr reqs. + DecrHits uint64 + // CasMisses number of CAS reqs against missing keys. + CasMisses uint64 + // CasHits number of successful CAS reqs. + CasHits uint64 + // CasBadval number of CAS reqs for which a key was found, but the CAS value did not match. + CasBadval uint64 + // TouchHits number of keys that have been touched with a new expiration time. + TouchHits uint64 + // TouchMisses number of items that have been touched and not found. + TouchMisses uint64 + // AuthCmds number of authentication commands handled, success or failure. + AuthCmds uint64 + // AuthErrors number of failed authentications. + AuthErrors uint64 + // IdleKicks number of connections closed due to reaching their idle timeout. + IdleKicks uint64 + // Evictions number of valid items removed from cache to free memory for new items. + Evictions uint64 + // Reclaimed number of times an entry was stored using memory from an expired entry. + Reclaimed uint64 + // BytesRead total number of bytes read by this server from network. + BytesRead uint64 + // BytesWritten total number of bytes sent by this server to network. + BytesWritten uint64 + // LimitMaxbytes number of bytes this server is allowed to use for storage. + LimitMaxbytes uint64 + // ListenDisabledNum number of times server has stopped accepting new connections (maxconns). + ListenDisabledNum uint64 + // TimeInListenDisabledUs number of microseconds in maxconns. + TimeInListenDisabledUs uint64 + // ConnYields number of times any connection yielded to another due to hitting the -R limit. + ConnYields uint64 + // HashBytes bytes currently used by hash tables. + HashBytes uint64 + // ExpiredUnfetched items pulled from LRU that were never touched by get/incr/append/etc before expiring. + ExpiredUnfetched uint64 + // EvictedUnfetched items evicted from LRU that were never touched by get/incr/append/etc. + EvictedUnfetched uint64 + // EvictedActive items evicted from LRU that had been hit recently but did not jump to top of LRU. + EvictedActive uint64 + // SlabsMoved total slab pages moved. + SlabsMoved uint64 + // CrawlerReclaimed total items freed by LRU Crawler. + CrawlerReclaimed uint64 + // CrawlerItemsChecked total items examined by LRU Crawler. + CrawlerItemsChecked uint64 + // LrutailReflocked times LRU tail was found with active ref. Items can be evicted to avoid OOM errors. + LrutailReflocked uint64 + // MovesToCold items moved from HOT/WARM to COLD LRU's. + MovesToCold uint64 + // MovesToWarm items moved from COLD to WARM LRU. + MovesToWarm uint64 + // MovesWithinLru items reshuffled within HOT or WARM LRU's. + MovesWithinLru uint64 + // DirectReclaims times worker threads had to directly reclaim or evict items. + DirectReclaims uint64 + // LruCrawlerStarts times an LRU crawler was started. + LruCrawlerStarts uint64 + // LruMaintainerJuggles number of times the LRU bg thread woke up. + LruMaintainerJuggles uint64 + // SlabReassignRescues items rescued from eviction in page move. + SlabReassignRescues uint64 + // SlabReassignEvictionsNomem valid items evicted during a page move (due to no free memory in slab). + SlabReassignEvictionsNomem uint64 + // SlabReassignChunkRescues individual sections of an item rescued during a page move. + SlabReassignChunkRescues uint64 + // SlabReassignInlineReclaim internal stat counter for when the page mover clears memory from the chunk freelist when it wasn't expecting to. + SlabReassignInlineReclaim uint64 + // SlabReassignBusyItems items busy during page move, requiring a retry before page can be moved. + SlabReassignBusyItems uint64 + // SlabReassignBusyDeletes items busy during page move, requiring deletion before page can be moved. + SlabReassignBusyDeletes uint64 + // LogWorkerDropped logs a worker never wrote due to full buf. + LogWorkerDropped uint64 + // LogWorkerWritten logs written by a worker, to be picked up. + LogWorkerWritten uint64 + // LogWatcherSkipped logs not sent to slow watchers. + LogWatcherSkipped uint64 + // LogWatcherSent logs written to watchers. + LogWatcherSent uint64 + // Libevent libevent string version. + Libevent string + // LruCrawlerRunning crawl in progress. + LruCrawlerRunning bool + // MallocFails number of malloc fails. + MallocFails uint64 + // LruBumpsDropped lru total bumps dropped. + LruBumpsDropped uint64 +} + +// StatsServers returns the general statistics of the servers +// retrieved with the `stats` command. +func (c *Client) StatsServers() (servers map[net.Addr]*ServerStats, err error) { + servers = make(map[net.Addr]*ServerStats) + + // addrs slice of the all the server adresses from the selector. + addrs := make([]net.Addr, 0) + + err = c.selector.Each( + func(addr net.Addr) error { + addrs = append(addrs, addr) + servers[addr] = new(ServerStats) + servers[addr].UnknownStats = make(map[string]string) + return nil + }, + ) + if err != nil { + return + } + + var wg sync.WaitGroup + wg.Add(len(addrs)) + for _, addr := range addrs { + go func(addr net.Addr) { + server := servers[addr] + c.statsFromAddr(server, addr) + wg.Done() + }(addr) + } + wg.Wait() + return +} + +func (c *Client) statsFromAddr(server *ServerStats, addr net.Addr) { + err := c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + if _, err := fmt.Fprintf(rw, "stats\r\n"); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + + for { + line, err := rw.ReadBytes('\n') + if err != nil { + return err + } + if bytes.Equal(line, resultEnd) { + return nil + } + // STAT \r\n + tkns := bytes.Split(line[5:len(line)-2], space) + err = parseSetStatValue(reflect.ValueOf(server), string(tkns[0]), string(tkns[1])) + if err != nil { + if err != errUnknownStat { + server.Errs.AppendError(err) + } else { + server.UnknownStats[string(tkns[0])] = string(tkns[1]) + } + } + } + }) + server.ServerErr = err + return +} + +// parseSetStatValue parses and sets the value of the stat to the corresponding struct field. +// +// In order to know the corresponding field of the stat, the snake_case stat name is converted to CamelCase +func parseSetStatValue(strPntr reflect.Value, stat, value string) error { + statCamel := toCamel(stat) + f := reflect.Indirect(strPntr).FieldByName(statCamel) + switch f.Kind() { + case reflect.Uint32: + uintv, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return errors.New("unable to parse uint value " + stat + ":" + err.Error()) + } + f.SetUint(uintv) + case reflect.Uint64: + uintv, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return errors.New("unable to parse uint value " + stat + ":" + err.Error()) + } + f.SetUint(uintv) + case reflect.Float64: + floatv, err := strconv.ParseFloat(value, 64) + if err != nil { + return errors.New("unable to parse float value for " + stat + ":" + err.Error()) + } + f.SetFloat(floatv) + case reflect.String: + f.SetString(value) + case reflect.Bool: + f.SetBool(value == "1") + default: + return errUnknownStat + } + return nil +} + +func toCamel(s string) string { + if s == "" { + return "" + } + // Compute number of replacements. + m := strings.Count(s, "_") + if m == 0 { + return string(unicode.ToUpper(rune(s[0]))) + s[1:] // avoid allocation + } + + // Apply replacements to buffer. + l := len(s) - m + if l == 0 { + return "" + } + t := make([]byte, l) + + w := 0 + start := 0 + for i := 0; i < m; i++ { + j := start + j += strings.Index(s[start:], "_") + if start != j { + t[w] = byte(unicode.ToUpper(rune(s[start]))) + w++ + w += copy(t[w:], s[start+1:j]) + } + start = j + 1 + } + if s[start:] != "" { + t[w] = byte(unicode.ToUpper(rune(s[start]))) + w++ + w += copy(t[w:], s[start+1:]) + } + return string(t[0:w]) +} diff --git a/memcache/stats_items.go b/memcache/stats_items.go new file mode 100644 index 00000000..1603dc8a --- /dev/null +++ b/memcache/stats_items.go @@ -0,0 +1,302 @@ +/* +Copyright 2019 gomemcache Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package memcache + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "net" + "reflect" + "regexp" + "strconv" + "sync" +) + +var cachedumpItem = regexp.MustCompile("ITEM (.*) \\[(\\d+) b; (\\d+) s\\]") + +// DumpKey information of a stored key. +type DumpKey struct { + // Key item key. + Key string + // Size item size (including key) in bytes. + Size uint64 + // Expiration expiration time, as a unix timestamp. + Expiration uint64 +} + +// StatsItemsServer information about item storage per slab class. +type StatsItemsServer struct { + // ServerErr irror if can't get the stats information. + ServerErr error + // StatsItemsSlabs statistics for each slab. + StatsItemsSlabs map[string]*StatsItems + // DumpErr error if can't cachedump. + DumpErr error +} + +// StatsItems information of the items in the slab. +type StatsItems struct { + // Errs contains the errors that occurred while parsing the response. + Errs errorsSlice + // Keys retrieved dumped keys. + Keys []DumpKey + // UnknownStats contains the stats that are not in the struct. This can be useful if the memcached developers add new stats in newer versions. + UnknownStats map[string]string + + // Number number of items presently stored in this class. Expired items are not automatically excluded. + Number uint64 + // NumberHot number of items presently stored in the HOT LRU. + NumberHot uint64 + // NumberWarm number of items presently stored in the WARM LRU. + NumberWarm uint64 + // NumberCold number of items presently stored in the COLD LRU. + NumberCold uint64 + // NumberTemp number of items presently stored in the TEMPORARY LRU. + NumberTemp uint64 + // AgeHot age of the oldest item in HOT LRU. + AgeHot uint64 + // AgeWarm age of the oldest item in WARM LRU. + AgeWarm uint64 + // Age age of the oldest item in the LRU. + Age uint64 + // Evicted number of times an item had to be evicted from the LRU before it expired. + Evicted uint64 + // EvictedNonzero number of times an item which had an explicit expire time set had to be evicted from the LRU before it expired. + EvictedNonzero uint64 + // EvictedTime seconds since the last access for the most recent item evicted from this class. Use this to judge how recently active your evicted data is. + EvictedTime uint64 + // Outofmemory number of times the underlying slab class was unable to store a new item. This means you are running with -M or an eviction failed. + Outofmemory uint64 + // Tailrepairs number of times we self-healed a slab with a refcount leak. If this counter is increasing a lot, please report your situation to the developers. + Tailrepairs uint64 + // Reclaimed number of times an entry was stored using memory from an expired entry. + Reclaimed uint64 + // ExpiredUnfetched number of expired items reclaimed from the LRU which were never touched after being set. + ExpiredUnfetched uint64 + // EvictedUnfetched number of valid items evicted from the LRU which were never touched after being set. + EvictedUnfetched uint64 + // EvictedActive number of valid items evicted from the LRU which were recently touched but were evicted before being moved to the top of the LRU again. + EvictedActive uint64 + // CrawlerReclaimed number of items freed by the LRU Crawler. + CrawlerReclaimed uint64 + // LrutailReflocked number of items found to be refcount locked in the LRU tail. + LrutailReflocked uint64 + // MovesToCold number of items moved from HOT or WARM into COLD. + MovesToCold uint64 + // MovesToWarm number of items moved from COLD to WARM. + MovesToWarm uint64 + // MovesWithinLru number of times active items were bumped within HOT or WARM. + MovesWithinLru uint64 + // DirectReclaims number of times worker threads had to directly pull LRU tails to find memory for a new item. + DirectReclaims uint64 + // HitsToHot number of keys that have been requested and found present in the HOT LRU. + HitsToHot uint64 + // HitsToWarm number of keys that have been requested and found present in the WARM LRU. + HitsToWarm uint64 + // HitsToCold number of keys that have been requested and found present in the COLD LRU. + HitsToCold uint64 + // HitsToTemp number of keys that have been requested and found present in each sub-LRU. + HitsToTemp uint64 +} + +// StatsItemsServers returns information about item storage per slab class of all the servers, +// retrieved with the `stats items` command. +// +// maxDumpKeys is the maximum number of keys that are going to be retrieved +// if maxDumpKeys < 0, doesn't dump the keys. +func (c *Client) StatsItemsServers(maxDumpKeys int) (servers map[net.Addr]*StatsItemsServer, err error) { + servers = make(map[net.Addr]*StatsItemsServer) + + // addrs slice of the all the server adresses from the selector. + addrs := make([]net.Addr, 0) + + err = c.selector.Each( + func(addr net.Addr) error { + addrs = append(addrs, addr) + servers[addr] = &StatsItemsServer{StatsItemsSlabs: make(map[string]*StatsItems)} + return nil + }, + ) + if err != nil { + return + } + + var wg sync.WaitGroup + wg.Add(len(addrs)) + for _, addr := range addrs { + go func(addr net.Addr) { + server := servers[addr] + c.statsItemsFromAddr(server, addr) + wg.Done() + }(addr) + } + wg.Wait() + + if maxDumpKeys >= 0 { + wg.Add(len(addrs)) + for _, addr := range addrs { + go func(addr net.Addr) { + server := servers[addr] + c.cachedumpFromAddr(server, maxDumpKeys, addr) + wg.Done() + }(addr) + } + wg.Wait() + } + return +} + +// From the protocol definition for the command stats items: +// +// ------------------------------------------------------------------------ +// Item statistics. +// +// CAVEAT: This section describes statistics which are subject to change in the +// future. +// +// The "stats" command with the argument of "items" returns information about +// item storage per slab class. The data is returned in the format: +// +// STAT items:: \r\n +// +// The server terminates this list with the line: +// +// END\r\n +// +// The slabclass aligns with class ids used by the "stats slabs" command. Where +// "stats slabs" describes size and memory usage, "stats items" shows higher +// level information. +// +// The following item values are defined as of writing. +// ------------------------------------------------------------------------ +func (c *Client) statsItemsFromAddr(sis *StatsItemsServer, addr net.Addr) { + err := c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + if _, err := fmt.Fprintf(rw, "stats items\r\n"); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + + for { + line, err := rw.ReadBytes('\n') + if err != nil { + return err + } + if bytes.Equal(line, resultEnd) { + return nil + } + + // STAT items:: + sis.parseStat(line[11 : len(line)-2]) + } + }) + sis.ServerErr = err + return +} + +func (sis *StatsItemsServer) parseStat(line []byte) { + // : + tkns := bytes.FieldsFunc(line, func(r rune) bool { + return r == ':' || r == ' ' + }) + slabclass := string(tkns[0]) + slab := sis.StatsItemsSlabs[slabclass] + if slab == nil { + slab = new(StatsItems) + slab.UnknownStats = make(map[string]string) + sis.StatsItemsSlabs[slabclass] = slab + } + err := parseSetStatValue(reflect.ValueOf(slab), string(tkns[1]), string(tkns[2])) + if err != nil { + if err != errUnknownStat { + slab.Errs.AppendError(err) + } else { + slab.UnknownStats[string(tkns[1])] = string(tkns[2]) + } + } +} + +func (c *Client) cachedumpFromAddr(sis *StatsItemsServer, maxDumpKeys int, addr net.Addr) { + var wg sync.WaitGroup + l := len(sis.StatsItemsSlabs) + if l == 0 { + return + } + err := c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + line, err := writeReadLine(rw, "stats cachedump\r\n") + if err != nil { + return err + } + if bytes.Equal(line, []byte("CLIENT_ERROR stats cachedump not allowed\r\n")) { + return ErrDumpDisable + } + return nil + }) + if err != nil { + sis.DumpErr = err + return + } + + wg.Add(l) + for slab, stats := range sis.StatsItemsSlabs { + go func(slab string, stats *StatsItems) { + c.cachedumpSlabFromAddr(slab, stats, maxDumpKeys, addr) + wg.Done() + }(slab, stats) + } + wg.Wait() + return +} + +func (c *Client) cachedumpSlabFromAddr(slab string, si *StatsItems, maxDumpKeys int, addr net.Addr) (err error) { + err = c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + if _, err := fmt.Fprintf(rw, "stats cachedump %s %d\r\n", slab, maxDumpKeys); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + + for { + line, err := rw.ReadBytes('\n') + if err != nil { + return err + } + if bytes.Equal(line, resultEnd) { + return nil + } + // ITEM [ b; s]\r\n + si.parseAddDumpKey(string(line)) + } + }) + return +} + +func (si *StatsItems) parseAddDumpKey(line string) { + tkns := cachedumpItem.FindStringSubmatch(line) + if len(tkns) != 4 { + si.Errs.AppendError(errors.New("regex didn't match response correctly")) + return + } + dk := DumpKey{Key: tkns[1]} + dk.Size, _ = strconv.ParseUint(tkns[2], 10, 64) + dk.Expiration, _ = strconv.ParseUint(tkns[3], 10, 64) + si.Keys = append(si.Keys, dk) +} diff --git a/memcache/stats_slabs.go b/memcache/stats_slabs.go new file mode 100644 index 00000000..4511fe22 --- /dev/null +++ b/memcache/stats_slabs.go @@ -0,0 +1,193 @@ +/* +Copyright 2019 gomemcache Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package memcache + +import ( + "bufio" + "bytes" + "fmt" + "net" + "reflect" + "strconv" + "sync" +) + +// StatsSlabsServer information broken down by slab about items stored in memcached, +// more centered to performance of a slab rather than counts of particular items. +type StatsSlabsServer struct { + // ServerErr error if can't get the stats information. + ServerErr error + // StatsSlabs statistics for each slab. + StatsSlabs map[string]*StatsSlab + // ActiveSlabs total number of slab classes allocated. + ActiveSlabs uint64 + // TotalMalloced total amount of memory allocated to slab pages. + TotalMalloced uint64 +} + +// StatsSlab statistics for one slab. +type StatsSlab struct { + // Errs contains the errors that occurred while parsing the response. + Errs errorsSlice + // UnknownStats contains the stats that are not in the struct. This can be useful if the memcached developers add new stats in newer versions. + UnknownStats map[string]string + + // ChunkSize the amount of space each chunk uses. One item will use one chunk of the appropriate size. + ChunkSize uint64 + // ChunksPerPage how many chunks exist within one page. A page by default is less than or equal to one megabyte in size. Slabs are allocated by page, then broken into chunks. + ChunksPerPage uint64 + // TotalPages total number of pages allocated to the slab class. + TotalPages uint64 + // TotalChunks total number of chunks allocated to the slab class. + TotalChunks uint64 + // GetHits total number of get requests serviced by this class. + GetHits uint64 + // CmdSet total number of set requests storing data in this class. + CmdSet uint64 + // DeleteHits total number of successful deletes from this class. + DeleteHits uint64 + // IncrHits total number of incrs modifying this class. + IncrHits uint64 + // DecrHits total number of decrs modifying this class. + DecrHits uint64 + // CasHits total number of CAS commands modifying this class. + CasHits uint64 + // CasBadval total number of CAS commands that failed to modify a value due to a bad CAS id. + CasBadval uint64 + // TouchHits total number of touches serviced by this class. + TouchHits uint64 + // UsedChunks how many chunks have been allocated to items. + UsedChunks uint64 + // FreeChunks chunks not yet allocated to items, or freed via delete. + FreeChunks uint64 + // FreeChunksEnd number of free chunks at the end of the last allocated page. + FreeChunksEnd uint64 + // MemRequested number of bytes requested to be stored in this slab[*]. + MemRequested uint64 + // ActiveSlabs total number of slab classes allocated. + ActiveSlabs uint64 + // TotalMalloced total amount of memory allocated to slab pages. + TotalMalloced uint64 +} + +// StatsSlabsServers returns information about the storage per slab class of the servers, +// retrieved with the `stats slabs` command. +func (c *Client) StatsSlabsServers() (servers map[net.Addr]*StatsSlabsServer, err error) { + servers = make(map[net.Addr]*StatsSlabsServer) + + // addrs slice of the all the server adresses from the selector. + addrs := make([]net.Addr, 0) + + err = c.selector.Each( + func(addr net.Addr) error { + addrs = append(addrs, addr) + servers[addr] = &StatsSlabsServer{StatsSlabs: make(map[string]*StatsSlab)} + return nil + }, + ) + if err != nil { + return + } + + var wg sync.WaitGroup + wg.Add(len(addrs)) + for _, addr := range addrs { + go func(addr net.Addr) { + server := servers[addr] + c.statsSlabsFromAddr(server, addr) + wg.Done() + }(addr) + } + wg.Wait() + return +} + +// From the protocol definition for the command stats items: +// +// Slab statistics +// --------------- +// CAVEAT: This section describes statistics which are subject to change in the +// future. +// +// The "stats" command with the argument of "slabs" returns information about +// each of the slabs created by memcached during runtime. This includes per-slab +// information along with some totals. The data is returned in the format: +// +// STAT : \r\n +// STAT \r\n +// +// The server terminates this list with the line: +// +// END\r\n +func (c *Client) statsSlabsFromAddr(sss *StatsSlabsServer, addr net.Addr) (err error) { + err = c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + if _, err := fmt.Fprintf(rw, "stats slabs\r\n"); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + + for { + line, err := rw.ReadBytes('\n') + if err != nil { + return err + } + if bytes.Equal(line, resultEnd) { + return err + } + // STAT : \r\n + // STAT \r\n + sss.parseStat(line[5 : len(line)-2]) + } + }) + sss.ServerErr = err + return +} + +func (sss *StatsSlabsServer) parseStat(line []byte) { + // : + // + tkns := bytes.FieldsFunc(line, func(r rune) bool { + return r == ':' || r == ' ' + }) + if len(tkns) == 2 { + if string(tkns[0]) == "active_slabs" { + sss.ActiveSlabs, _ = strconv.ParseUint(string(tkns[1]), 10, 64) + } else { + sss.TotalMalloced, _ = strconv.ParseUint(string(tkns[1]), 10, 64) + } + return + } + + slabclass := string(tkns[0]) + // + slab := sss.StatsSlabs[slabclass] + if slab == nil { + slab = new(StatsSlab) + slab.UnknownStats = make(map[string]string) + sss.StatsSlabs[slabclass] = slab + } + err := parseSetStatValue(reflect.ValueOf(slab), string(tkns[1]), string(tkns[2])) + if err != nil { + if err != errUnknownStat { + slab.Errs.AppendError(err) + } else { + slab.UnknownStats[string(tkns[1])] = string(tkns[2]) + } + } +}