// ToJSON returns a JSON representation of the object. func (n *ActionNode) ToJSON() string { result := jscfg.ToJSON(n) + "\n" if n.Args == nil { result += "{}\n" } else { result += jscfg.ToJSON(n.Args) + "\n" } if n.Reply == nil { result += "{}\n" } else { result += jscfg.ToJSON(n.Reply) + "\n" } return result }
func commandVtGateSplitQuery(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { server := subFlags.String("server", "", "VtGate server to connect to") bindVariables := newBindvars(subFlags) connectTimeout := subFlags.Duration("connect_timeout", 30*time.Second, "Connection timeout for vtgate client") splitColumn := subFlags.String("split_column", "", "force the use of this column to split the query") splitCount := subFlags.Int("split_count", 16, "number of splits to generate") keyspace := subFlags.String("keyspace", "", "keyspace to send query to") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { return fmt.Errorf("the <sql> argument is required for the VtGateSplitQuery command") } vtgateConn, err := vtgateconn.Dial(ctx, *server, *connectTimeout) if err != nil { return fmt.Errorf("error connecting to vtgate '%v': %v", *server, err) } defer vtgateConn.Close() r, err := vtgateConn.SplitQuery(ctx, *keyspace, tproto.BoundQuery{ Sql: subFlags.Arg(0), BindVariables: *bindVariables, }, *splitColumn, *splitCount) if err != nil { return fmt.Errorf("SplitQuery failed: %v", err) } wr.Logger().Printf("%v\n", jscfg.ToJSON(r)) return nil }
// UpdateShardReplicationFields is part of the topo.Server interface func (zkts *Server) UpdateShardReplicationFields(ctx context.Context, cell, keyspace, shard string, update func(*pb.ShardReplication) error) error { // create the parent directory to be sure it's here zkDir := path.Join("/zk", cell, "vt", "replication", keyspace) if _, err := zk.CreateRecursive(zkts.zconn, zkDir, "", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)); err != nil && !zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { return err } // now update the data zkPath := shardReplicationPath(cell, keyspace, shard) f := func(oldValue string, oldStat zk.Stat) (string, error) { sr := &pb.ShardReplication{} if oldValue != "" { if err := json.Unmarshal([]byte(oldValue), sr); err != nil { return "", err } } if err := update(sr); err != nil { return "", err } return jscfg.ToJSON(sr), nil } err := zkts.zconn.RetryChange(zkPath, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), f) if err != nil { if zookeeper.IsError(err, zookeeper.ZNONODE) { err = topo.ErrNoNode } return err } return nil }
// CreateShard implements topo.Server. func (s *Server) CreateShard(ctx context.Context, keyspace, shard string, value *topo.Shard) error { data := jscfg.ToJSON(value) global := s.getGlobal() resp, err := global.Create(shardFilePath(keyspace, shard), data, 0 /* ttl */) if err != nil { return convertError(err) } if err := initLockFile(global, shardDirPath(keyspace, shard)); err != nil { return err } // We don't return ErrBadResponse in this case because the Create() suceeeded // and we don't really need the version to satisfy our contract - we're only // logging it. version := int64(-1) if resp.Node != nil { version = int64(resp.Node.ModifiedIndex) } event.Dispatch(&events.ShardChange{ ShardInfo: *topo.NewShardInfo(keyspace, shard, value, version), Status: "created", }) return nil }
// CreateShard is part of the topo.Server interface func (zkts *Server) CreateShard(ctx context.Context, keyspace, shard string, value *pb.Shard) error { shardPath := path.Join(globalKeyspacesPath, keyspace, "shards", shard) pathList := []string{ shardPath, path.Join(shardPath, "action"), path.Join(shardPath, "actionlog"), } alreadyExists := false for i, zkPath := range pathList { c := "" if i == 0 { c = jscfg.ToJSON(value) } _, err := zk.CreateRecursive(zkts.zconn, zkPath, c, 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) if err != nil { if zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { alreadyExists = true } else { return fmt.Errorf("error creating shard: %v %v", zkPath, err) } } } if alreadyExists { return topo.ErrNodeExists } event.Dispatch(&events.ShardChange{ ShardInfo: *topo.NewShardInfo(keyspace, shard, value, -1), Status: "created", }) return nil }
func TestHandlePathShard(t *testing.T) { input := path.Join(explorerRoot, "global", shardDirPath("test_keyspace", "-80")) cells := []string{"cell1", "cell2", "cell3"} keyspace := &pb.Keyspace{} shard := &pb.Shard{} want := jscfg.ToJSON(shard) ctx := context.Background() ts := newTestServer(t, cells) if err := ts.CreateKeyspace(ctx, "test_keyspace", keyspace); err != nil { t.Fatalf("CreateKeyspace error: %v", err) } if err := ts.CreateShard(ctx, "test_keyspace", "-80", shard); err != nil { t.Fatalf("CreateShard error: %v", err) } m := &mockActionRepo{} ex := NewExplorer(ts) result := ex.HandlePath(m, input, nil) exResult := result.(*explorerResult) if got := exResult.Data; got != want { t.Errorf("HandlePath(%q) = %q, want %q", input, got, want) } if m.shardActions == nil { t.Errorf("ActionRepository.PopulateShardActions not called") } if m.keyspace != "test_keyspace" { t.Errorf("ActionRepository called with keyspace %q, want %q", m.keyspace, "test_keyspace") } if m.shard != "-80" { t.Errorf("ActionRepository called with shard %q, want %q", m.shard, "-80") } }
// UpdateEndPoints is part of the topo.Server interface func (zkts *Server) UpdateEndPoints(ctx context.Context, cell, keyspace, shard string, tabletType topo.TabletType, addrs *pb.EndPoints, existingVersion int64) error { path := zkPathForVtName(cell, keyspace, shard, tabletType) data := jscfg.ToJSON(addrs) if existingVersion == -1 { // Update or create unconditionally. _, err := zk.CreateRecursive(zkts.zconn, path, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) if err != nil { if zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { // Node already exists - just stomp away. Multiple writers shouldn't be here. // We use RetryChange here because it won't update the node unnecessarily. f := func(oldValue string, oldStat zk.Stat) (string, error) { return data, nil } err = zkts.zconn.RetryChange(path, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), f) } } return err } // Compare And Set _, err := zkts.zconn.Set(path, data, int(existingVersion)) if err != nil { if zookeeper.IsError(err, zookeeper.ZBADVERSION) { err = topo.ErrBadVersion } else if zookeeper.IsError(err, zookeeper.ZNONODE) { err = topo.ErrNoNode } } return err }
func commandVtGateExecuteShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { server := subFlags.String("server", "", "VtGate server to connect to") bindVariables := newBindvars(subFlags) connectTimeout := subFlags.Duration("connect_timeout", 30*time.Second, "Connection timeout for vtgate client") tabletType := subFlags.String("tablet_type", "master", "tablet type to query") keyspace := subFlags.String("keyspace", "", "keyspace to send query to") shardsStr := subFlags.String("shards", "", "comma-separated list of shards to send query to") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { return fmt.Errorf("the <sql> argument is required for the VtGateExecuteShard command") } t, err := parseTabletType(*tabletType, []topo.TabletType{topo.TYPE_MASTER, topo.TYPE_REPLICA, topo.TYPE_RDONLY}) if err != nil { return err } var shards []string if *shardsStr != "" { shards = strings.Split(*shardsStr, ",") } vtgateConn, err := vtgateconn.Dial(ctx, *server, *connectTimeout) if err != nil { return fmt.Errorf("error connecting to vtgate '%v': %v", *server, err) } defer vtgateConn.Close() qr, err := vtgateConn.ExecuteShard(ctx, subFlags.Arg(0), *keyspace, shards, *bindVariables, t) if err != nil { return fmt.Errorf("Execute failed: %v", err) } wr.Logger().Printf("%v\n", jscfg.ToJSON(qr)) return nil }
// CreateEndPoints implements topo.Server. func (s *Server) CreateEndPoints(ctx context.Context, cellName, keyspace, shard string, tabletType topo.TabletType, addrs *pb.EndPoints) error { cell, err := s.getCell(cellName) if err != nil { return err } // Set only if it doesn't exist. _, err = cell.Create(endPointsFilePath(keyspace, shard, string(tabletType)), jscfg.ToJSON(addrs), 0 /* ttl */) return convertError(err) }
// UpdateSrvKeyspace is part of the topo.Server interface func (zkts *Server) UpdateSrvKeyspace(ctx context.Context, cell, keyspace string, srvKeyspace *topo.SrvKeyspace) error { path := zkPathForVtKeyspace(cell, keyspace) data := jscfg.ToJSON(srvKeyspace) _, err := zkts.zconn.Set(path, data, -1) if zookeeper.IsError(err, zookeeper.ZNONODE) { _, err = zk.CreateRecursive(zkts.zconn, path, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) } return err }
// CreateEndPoints is part of the topo.Server interface func (zkts *Server) CreateEndPoints(ctx context.Context, cell, keyspace, shard string, tabletType topo.TabletType, addrs *pb.EndPoints) error { path := zkPathForVtName(cell, keyspace, shard, tabletType) data := jscfg.ToJSON(addrs) // Create only if it doesn't exist. _, err := zk.CreateRecursive(zkts.zconn, path, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) if zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { err = topo.ErrNodeExists } return err }
// UpdateSrvKeyspace implements topo.Server. func (s *Server) UpdateSrvKeyspace(ctx context.Context, cellName, keyspace string, srvKeyspace *topo.SrvKeyspace) error { cell, err := s.getCell(cellName) if err != nil { return err } data := jscfg.ToJSON(srvKeyspace) _, err = cell.Set(srvKeyspaceFilePath(keyspace), data, 0 /* ttl */) return convertError(err) }
// UpdateSrvShard implements topo.Server. func (s *Server) UpdateSrvShard(ctx context.Context, cellName, keyspace, shard string, srvShard *pb.SrvShard) error { cell, err := s.getCell(cellName) if err != nil { return err } data := jscfg.ToJSON(srvShard) _, err = cell.Set(srvShardFilePath(keyspace, shard), data, 0 /* ttl */) return convertError(err) }
// updateEndPoints updates the EndPoints file only if the version matches. func (s *Server) updateEndPoints(cellName, keyspace, shard string, tabletType topo.TabletType, addrs *pb.EndPoints, version int64) error { cell, err := s.getCell(cellName) if err != nil { return err } data := jscfg.ToJSON(addrs) _, err = cell.CompareAndSwap(endPointsFilePath(keyspace, shard, string(tabletType)), data, 0, /* ttl */ "" /* prevValue */, uint64(version)) return convertError(err) }
// init phase: // - read the destination keyspace, make sure it has 'servedFrom' values func (scw *SplitCloneWorker) init(ctx context.Context) error { scw.setState(WorkerStateInit) var err error // read the keyspace and validate it scw.keyspaceInfo, err = scw.wr.TopoServer().GetKeyspace(ctx, scw.keyspace) if err != nil { return fmt.Errorf("cannot read keyspace %v: %v", scw.keyspace, err) } // find the OverlappingShards in the keyspace osList, err := topotools.FindOverlappingShards(ctx, scw.wr.TopoServer(), scw.keyspace) if err != nil { return fmt.Errorf("cannot FindOverlappingShards in %v: %v", scw.keyspace, err) } // find the shard we mentioned in there, if any os := topotools.OverlappingShardsForShard(osList, scw.shard) if os == nil { return fmt.Errorf("the specified shard %v/%v is not in any overlapping shard", scw.keyspace, scw.shard) } scw.wr.Logger().Infof("Found overlapping shards: %v\n", jscfg.ToJSON(os)) // one side should have served types, the other one none, // figure out wich is which, then double check them all if len(os.Left[0].ServedTypes) > 0 { scw.sourceShards = os.Left scw.destinationShards = os.Right } else { scw.sourceShards = os.Right scw.destinationShards = os.Left } // validate all serving types servingTypes := []pb.TabletType{pb.TabletType_MASTER, pb.TabletType_REPLICA, pb.TabletType_RDONLY} for _, st := range servingTypes { for _, si := range scw.sourceShards { if si.GetServedType(st) == nil { return fmt.Errorf("source shard %v/%v is not serving type %v", si.Keyspace(), si.ShardName(), st) } } } for _, si := range scw.destinationShards { if len(si.ServedTypes) > 0 { return fmt.Errorf("destination shard %v/%v is serving some types", si.Keyspace(), si.ShardName()) } } return nil }
// UpdateEndPoints implements topo.Server. func (s *Server) UpdateEndPoints(ctx context.Context, cellName, keyspace, shard string, tabletType topo.TabletType, addrs *pb.EndPoints, existingVersion int64) error { cell, err := s.getCell(cellName) if err != nil { return err } if existingVersion == -1 { // Set unconditionally. _, err := cell.Set(endPointsFilePath(keyspace, shard, string(tabletType)), jscfg.ToJSON(addrs), 0 /* ttl */) return convertError(err) } // Update only if version matches. return s.updateEndPoints(cellName, keyspace, shard, tabletType, addrs, existingVersion) }
func TestExtraFieldsJson(t *testing.T) { swra := &SlaveWasRestartedArgs{ Parent: topo.TabletAlias{ Uid: 1, Cell: "aa", }, } data := jscfg.ToJSON(swra) output := &slaveWasRestartedTestArgs{} decoder := json.NewDecoder(strings.NewReader(data)) err := decoder.Decode(output) if err != nil { t.Errorf("Cannot re-decode struct without field: %v", err) } }
// UpdateShard is part of the topo.Server interface func (zkts *Server) UpdateShard(ctx context.Context, si *topo.ShardInfo, existingVersion int64) (int64, error) { shardPath := path.Join(globalKeyspacesPath, si.Keyspace(), "shards", si.ShardName()) stat, err := zkts.zconn.Set(shardPath, jscfg.ToJSON(si.Shard), int(existingVersion)) if err != nil { if zookeeper.IsError(err, zookeeper.ZNONODE) { err = topo.ErrNoNode } return -1, err } event.Dispatch(&events.ShardChange{ ShardInfo: *si, Status: "updated", }) return int64(stat.Version()), nil }
// UpdateKeyspace is part of the topo.Server interface func (zkts *Server) UpdateKeyspace(ctx context.Context, ki *topo.KeyspaceInfo, existingVersion int64) (int64, error) { keyspacePath := path.Join(globalKeyspacesPath, ki.KeyspaceName()) data := jscfg.ToJSON(ki.Keyspace) stat, err := zkts.zconn.Set(keyspacePath, data, int(existingVersion)) if err != nil { if zookeeper.IsError(err, zookeeper.ZNONODE) { err = topo.ErrNoNode } return -1, err } event.Dispatch(&events.KeyspaceChange{ KeyspaceInfo: *ki, Status: "updated", }) return int64(stat.Version()), nil }
// UpdateShard implements topo.Server. func (s *Server) UpdateShard(ctx context.Context, si *topo.ShardInfo, existingVersion int64) (int64, error) { data := jscfg.ToJSON(si.Shard) resp, err := s.getGlobal().CompareAndSwap(shardFilePath(si.Keyspace(), si.ShardName()), data, 0 /* ttl */, "" /* prevValue */, uint64(existingVersion)) if err != nil { return -1, convertError(err) } if resp.Node == nil { return -1, ErrBadResponse } event.Dispatch(&events.ShardChange{ ShardInfo: *si, Status: "updated", }) return int64(resp.Node.ModifiedIndex), nil }
func (s *Server) updateShardReplication(sri *topo.ShardReplicationInfo, existingVersion int64) (int64, error) { cell, err := s.getCell(sri.Cell()) if err != nil { return -1, err } data := jscfg.ToJSON(sri.ShardReplication) resp, err := cell.CompareAndSwap(shardReplicationFilePath(sri.Keyspace(), sri.Shard()), data, 0 /* ttl */, "" /* prevValue */, uint64(existingVersion)) if err != nil { return -1, convertError(err) } if resp.Node == nil { return -1, ErrBadResponse } return int64(resp.Node.ModifiedIndex), nil }
func (s *Server) createShardReplication(sri *topo.ShardReplicationInfo) (int64, error) { cell, err := s.getCell(sri.Cell()) if err != nil { return -1, err } data := jscfg.ToJSON(sri.ShardReplication) resp, err := cell.Create(shardReplicationFilePath(sri.Keyspace(), sri.Shard()), data, 0 /* ttl */) if err != nil { return -1, convertError(err) } if resp.Node == nil { return -1, ErrBadResponse } return int64(resp.Node.ModifiedIndex), nil }
// CreateTablet implements topo.Server. func (s *Server) CreateTablet(ctx context.Context, tablet *topo.Tablet) error { cell, err := s.getCell(tablet.Alias.Cell) if err != nil { return err } data := jscfg.ToJSON(tablet) _, err = cell.Create(tabletFilePath(tablet.Alias.String()), data, 0 /* ttl */) if err != nil { return convertError(err) } event.Dispatch(&events.TabletChange{ Tablet: *tablet, Status: "created", }) return nil }
// UpdateSrvShard is part of the topo.Server interface func (zkts *Server) UpdateSrvShard(ctx context.Context, cell, keyspace, shard string, srvShard *topo.SrvShard) error { path := zkPathForVtShard(cell, keyspace, shard) data := jscfg.ToJSON(srvShard) // Update or create unconditionally. _, err := zk.CreateRecursive(zkts.zconn, path, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) if err != nil { if zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { // Node already exists - just stomp away. Multiple writers shouldn't be here. // We use RetryChange here because it won't update the node unnecessarily. f := func(oldValue string, oldStat zk.Stat) (string, error) { return data, nil } err = zkts.zconn.RetryChange(path, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), f) } } return err }
func TestMissingFieldsJson(t *testing.T) { swra := &slaveWasRestartedTestArgs{ Parent: topo.TabletAlias{ Uid: 1, Cell: "aa", }, ExpectedMasterAddr: "a1", ExpectedMasterIPAddr: "i1", ScrapStragglers: true, } data := jscfg.ToJSON(swra) output := &SlaveWasRestartedArgs{} decoder := json.NewDecoder(strings.NewReader(data)) err := decoder.Decode(output) if err != nil { t.Errorf("Cannot re-decode struct without field: %v", err) } }
func commandVtTabletExecute(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { transactionID := subFlags.Int("transaction_id", 0, "transaction id to use, if inside a transaction.") bindVariables := newBindvars(subFlags) keyspace := subFlags.String("keyspace", "", "keyspace the tablet belongs to") shard := subFlags.String("shard", "", "shard the tablet belongs to") tabletType := subFlags.String("tablet_type", "unknown", "tablet type we expect from the tablet (use unknown to use sessionId)") connectTimeout := subFlags.Duration("connect_timeout", 30*time.Second, "Connection timeout for vttablet client") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { return fmt.Errorf("the <tablet_alis> and <sql> arguments are required for the VtTabletExecute command") } tt, err := parseTabletType3(*tabletType) if err != nil { return err } tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } tabletInfo, err := wr.TopoServer().GetTablet(ctx, tabletAlias) if err != nil { return err } ep, err := tabletInfo.EndPoint() if err != nil { return fmt.Errorf("cannot get EndPoint from tablet record: %v", err) } conn, err := tabletconn.GetDialer()(ctx, ep, *keyspace, *shard, tt, *connectTimeout) if err != nil { return fmt.Errorf("cannot connect to tablet %v: %v", tabletAlias, err) } defer conn.Close() qr, err := conn.Execute(ctx, subFlags.Arg(1), *bindVariables, int64(*transactionID)) if err != nil { return fmt.Errorf("Execute failed: %v", err) } wr.Logger().Printf("%v\n", jscfg.ToJSON(qr)) return nil }
// UpdateTablet implements topo.Server. func (s *Server) UpdateTablet(ctx context.Context, ti *topo.TabletInfo, existingVersion int64) (int64, error) { cell, err := s.getCell(ti.Alias.Cell) if err != nil { return -1, err } data := jscfg.ToJSON(ti.Tablet) resp, err := cell.CompareAndSwap(tabletFilePath(ti.Alias.String()), data, 0 /* ttl */, "" /* prevValue */, uint64(existingVersion)) if err != nil { return -1, convertError(err) } if resp.Node == nil { return -1, ErrBadResponse } event.Dispatch(&events.TabletChange{ Tablet: *ti.Tablet, Status: "updated", }) return int64(resp.Node.ModifiedIndex), nil }
func TestHandlePathTablet(t *testing.T) { input := path.Join(explorerRoot, "cell1", tabletDirPath("cell1-0000000123")) cells := []string{"cell1", "cell2", "cell3"} tablet := &topo.Tablet{ Alias: topo.TabletAlias{Cell: "cell1", Uid: 123}, Hostname: "example.com", Portmap: map[string]int{"vt": 4321}, } want := jscfg.ToJSON(tablet) ctx := context.Background() ts := newTestServer(t, cells) if err := ts.CreateTablet(ctx, tablet); err != nil { t.Fatalf("CreateTablet error: %v", err) } m := &mockActionRepo{} ex := NewExplorer(ts) result := ex.HandlePath(m, input, nil) exResult := result.(*explorerResult) if got := exResult.Data; got != want { t.Errorf("HandlePath(%q) = %q, want %q", input, got, want) } wantLinks := map[string]template.URL{ "status": template.URL("http://example.com:4321/debug/status"), } for k, want := range wantLinks { if got := exResult.Links[k]; got != want { t.Errorf("Links[%q] = %v, want %v", k, got, want) } } if m.tabletActions == nil { t.Errorf("ActionRepository.PopulateTabletActions not called") } if m.tablet != "cell1-0000000123" { t.Errorf("ActionRepository called with tablet %q, want %q", m.tablet, "cell1-0000000123") } }
func (zkts *Server) updateTabletEndpoint(oldValue string, oldStat zk.Stat, addr *pb.EndPoint) (newValue string, err error) { if oldStat == nil { // The incoming object doesn't exist - we haven't been placed in the serving // graph yet, so don't update. Assume the next process that rebuilds the graph // will get the updated tablet location. return "", errSkipUpdate } var addrs *pb.EndPoints if oldValue != "" { addrs = &pb.EndPoints{} if len(oldValue) > 0 { if err := json.Unmarshal([]byte(oldValue), addrs); err != nil { return "", fmt.Errorf("EndPoints unmarshal failed: %v %v", oldValue, err) } } foundTablet := false for i, entry := range addrs.Entries { if entry.Uid == addr.Uid { foundTablet = true if !topo.EndPointEquality(entry, addr) { addrs.Entries[i] = addr } break } } if !foundTablet { addrs.Entries = append(addrs.Entries, addr) } } else { addrs = topo.NewEndPoints() addrs.Entries = append(addrs.Entries, addr) } return jscfg.ToJSON(addrs), nil }
// UpdateTabletFields is part of the topo.Server interface func (zkts *Server) UpdateTabletFields(ctx context.Context, tabletAlias topo.TabletAlias, update func(*topo.Tablet) error) error { // Store the last tablet value so we can log it if the change succeeds. var lastTablet *topo.Tablet zkTabletPath := TabletPathForAlias(tabletAlias) f := func(oldValue string, oldStat zk.Stat) (string, error) { if oldValue == "" { return "", fmt.Errorf("no data for tablet addr update: %v", tabletAlias) } tablet, err := tabletFromJSON(oldValue) if err != nil { return "", err } if err := update(tablet); err != nil { return "", err } lastTablet = tablet return jscfg.ToJSON(tablet), nil } err := zkts.zconn.RetryChange(zkTabletPath, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), f) if err != nil { if zookeeper.IsError(err, zookeeper.ZNONODE) { err = topo.ErrNoNode } return err } if lastTablet != nil { event.Dispatch(&events.TabletChange{ Tablet: *lastTablet, Status: "updated", }) } return nil }