Browse Source

Merge pull request #5138 from heyitsanthony/v2api-refactor

etcdserver: v2api refactor
Anthony Romano 9 years ago
parent
commit
20db51bfb2
5 changed files with 328 additions and 169 deletions
  1. 129 0
      etcdserver/apply_v2.go
  2. 50 167
      etcdserver/server.go
  3. 20 2
      etcdserver/server_test.go
  4. 129 0
      etcdserver/v2_server.go
  5. 0 0
      etcdserver/v3_server.go

+ 129 - 0
etcdserver/apply_v2.go

@@ -0,0 +1,129 @@
+// Copyright 2016 CoreOS, 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 etcdserver
+
+import (
+	"encoding/json"
+	"path"
+	"time"
+
+	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
+	"github.com/coreos/etcd/etcdserver/membership"
+	"github.com/coreos/etcd/pkg/pbutil"
+	"github.com/coreos/etcd/store"
+	"github.com/coreos/go-semver/semver"
+)
+
+// applierV2 is the interface for processing V2 raft messages
+type applierV2 interface {
+	Delete(r *pb.Request) Response
+	Post(r *pb.Request) Response
+	Put(r *pb.Request) Response
+	QGet(r *pb.Request) Response
+	Sync(r *pb.Request) Response
+}
+
+type applierV2store struct{ s *EtcdServer }
+
+func (a *applierV2store) Delete(r *pb.Request) Response {
+	switch {
+	case r.PrevIndex > 0 || r.PrevValue != "":
+		return toResponse(a.s.store.CompareAndDelete(r.Path, r.PrevValue, r.PrevIndex))
+	default:
+		return toResponse(a.s.store.Delete(r.Path, r.Dir, r.Recursive))
+	}
+}
+
+func (a *applierV2store) Post(r *pb.Request) Response {
+	return toResponse(a.s.store.Create(r.Path, r.Dir, r.Val, true, toTTLOptions(r)))
+}
+
+func (a *applierV2store) Put(r *pb.Request) Response {
+	ttlOptions := toTTLOptions(r)
+	exists, existsSet := pbutil.GetBool(r.PrevExist)
+	switch {
+	case existsSet:
+		if exists {
+			if r.PrevIndex == 0 && r.PrevValue == "" {
+				return toResponse(a.s.store.Update(r.Path, r.Val, ttlOptions))
+			} else {
+				return toResponse(a.s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
+			}
+		}
+		return toResponse(a.s.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions))
+	case r.PrevIndex > 0 || r.PrevValue != "":
+		return toResponse(a.s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
+	default:
+		if storeMemberAttributeRegexp.MatchString(r.Path) {
+			id := membership.MustParseMemberIDFromKey(path.Dir(r.Path))
+			var attr membership.Attributes
+			if err := json.Unmarshal([]byte(r.Val), &attr); err != nil {
+				plog.Panicf("unmarshal %s should never fail: %v", r.Val, err)
+			}
+			a.s.cluster.UpdateAttributes(id, attr)
+			// return an empty response since there is no consumer.
+			return Response{}
+		}
+		if r.Path == membership.StoreClusterVersionKey() {
+			a.s.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val)))
+			// return an empty response since there is no consumer.
+			return Response{}
+		}
+		return toResponse(a.s.store.Set(r.Path, r.Dir, r.Val, ttlOptions))
+	}
+}
+
+func (a *applierV2store) QGet(r *pb.Request) Response {
+	return toResponse(a.s.store.Get(r.Path, r.Recursive, r.Sorted))
+}
+
+func (a *applierV2store) Sync(r *pb.Request) Response {
+	a.s.store.DeleteExpiredKeys(time.Unix(0, r.Time))
+	return Response{}
+}
+
+// applyV2Request interprets r as a call to store.X and returns a Response interpreted
+// from store.Event
+func (s *EtcdServer) applyV2Request(r *pb.Request) Response {
+	toTTLOptions(r)
+	switch r.Method {
+	case "POST":
+		return s.applyV2.Post(r)
+	case "PUT":
+		return s.applyV2.Put(r)
+	case "DELETE":
+		return s.applyV2.Delete(r)
+	case "QGET":
+		return s.applyV2.QGet(r)
+	case "SYNC":
+		return s.applyV2.Sync(r)
+	default:
+		// This should never be reached, but just in case:
+		return Response{err: ErrUnknownMethod}
+	}
+}
+
+func toTTLOptions(r *pb.Request) store.TTLOptionSet {
+	refresh, _ := pbutil.GetBool(r.Refresh)
+	ttlOptions := store.TTLOptionSet{Refresh: refresh}
+	if r.Expiration != 0 {
+		ttlOptions.ExpireTime = time.Unix(0, r.Expiration)
+	}
+	return ttlOptions
+}
+
+func toResponse(ev *store.Event, err error) Response {
+	return Response{Event: ev, err: err}
+}

+ 50 - 167
etcdserver/server.go

@@ -178,6 +178,8 @@ type EtcdServer struct {
 
 	store store.Store
 
+	applyV2 applierV2
+
 	applyV3    applierV3
 	kv         dstorage.ConsistentWatchableKV
 	lessor     lease.Lessor
@@ -382,6 +384,8 @@ func NewServer(cfg *ServerConfig) (srv *EtcdServer, err error) {
 		msgSnapC:      make(chan raftpb.Message, maxInFlightMsgSnap),
 	}
 
+	srv.applyV2 = &applierV2store{srv}
+
 	srv.be = be
 	srv.lessor = lease.NewLessor(srv.be)
 	srv.kv = dstorage.New(srv.be, srv.lessor, &srv.consistIndex)
@@ -731,70 +735,6 @@ func (s *EtcdServer) stopWithDelay(d time.Duration, err error) {
 // when the server is stopped.
 func (s *EtcdServer) StopNotify() <-chan struct{} { return s.done }
 
-// Do interprets r and performs an operation on s.store according to r.Method
-// and other fields. If r.Method is "POST", "PUT", "DELETE", or a "GET" with
-// Quorum == true, r will be sent through consensus before performing its
-// respective operation. Do will block until an action is performed or there is
-// an error.
-func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error) {
-	r.ID = s.reqIDGen.Next()
-	if r.Method == "GET" && r.Quorum {
-		r.Method = "QGET"
-	}
-	switch r.Method {
-	case "POST", "PUT", "DELETE", "QGET":
-		data, err := r.Marshal()
-		if err != nil {
-			return Response{}, err
-		}
-		ch := s.w.Register(r.ID)
-
-		// TODO: benchmark the cost of time.Now()
-		// might be sampling?
-		start := time.Now()
-		s.r.Propose(ctx, data)
-
-		proposePending.Inc()
-		defer proposePending.Dec()
-
-		select {
-		case x := <-ch:
-			proposeDurations.Observe(float64(time.Since(start)) / float64(time.Second))
-			resp := x.(Response)
-			return resp, resp.err
-		case <-ctx.Done():
-			proposeFailed.Inc()
-			s.w.Trigger(r.ID, nil) // GC wait
-			return Response{}, s.parseProposeCtxErr(ctx.Err(), start)
-		case <-s.done:
-			return Response{}, ErrStopped
-		}
-	case "GET":
-		switch {
-		case r.Wait:
-			wc, err := s.store.Watch(r.Path, r.Recursive, r.Stream, r.Since)
-			if err != nil {
-				return Response{}, err
-			}
-			return Response{Watcher: wc}, nil
-		default:
-			ev, err := s.store.Get(r.Path, r.Recursive, r.Sorted)
-			if err != nil {
-				return Response{}, err
-			}
-			return Response{Event: ev}, nil
-		}
-	case "HEAD":
-		ev, err := s.store.Get(r.Path, r.Recursive, r.Sorted)
-		if err != nil {
-			return Response{}, err
-		}
-		return Response{Event: ev}, nil
-	default:
-		return Response{}, ErrUnknownMethod
-	}
-}
-
 func (s *EtcdServer) SelfStats() []byte { return s.stats.JSON() }
 
 func (s *EtcdServer) LeaderStats() []byte {
@@ -1025,48 +965,7 @@ func (s *EtcdServer) apply(es []raftpb.Entry, confState *raftpb.ConfState) (uint
 		e := es[i]
 		switch e.Type {
 		case raftpb.EntryNormal:
-			// raft state machine may generate noop entry when leader confirmation.
-			// skip it in advance to avoid some potential bug in the future
-			if len(e.Data) == 0 {
-				select {
-				case s.forceVersionC <- struct{}{}:
-				default:
-				}
-				break
-			}
-
-			var raftReq pb.InternalRaftRequest
-			if !pbutil.MaybeUnmarshal(&raftReq, e.Data) { // backward compatible
-				var r pb.Request
-				pbutil.MustUnmarshal(&r, e.Data)
-				s.w.Trigger(r.ID, s.applyRequest(r))
-			} else if raftReq.V2 != nil {
-				req := raftReq.V2
-				s.w.Trigger(req.ID, s.applyRequest(*req))
-			} else {
-				// do not re-apply applied entries.
-				if e.Index <= s.consistIndex.ConsistentIndex() {
-					break
-				}
-				// set the consistent index of current executing entry
-				s.consistIndex.setConsistentIndex(e.Index)
-				ar := s.applyV3Request(&raftReq)
-				if ar.err != ErrNoSpace || len(s.alarmStore.Get(pb.AlarmType_NOSPACE)) > 0 {
-					s.w.Trigger(raftReq.ID, ar)
-					break
-				}
-				plog.Errorf("applying raft message exceeded backend quota")
-				go func() {
-					a := &pb.AlarmRequest{
-						MemberID: uint64(s.ID()),
-						Action:   pb.AlarmRequest_ACTIVATE,
-						Alarm:    pb.AlarmType_NOSPACE,
-					}
-					r := pb.InternalRaftRequest{Alarm: a}
-					s.processInternalRaftRequest(context.TODO(), r)
-					s.w.Trigger(raftReq.ID, ar)
-				}()
-			}
+			s.applyEntryNormal(&e)
 		case raftpb.EntryConfChange:
 			var cc raftpb.ConfChange
 			pbutil.MustUnmarshal(&cc, e.Data)
@@ -1083,70 +982,54 @@ func (s *EtcdServer) apply(es []raftpb.Entry, confState *raftpb.ConfState) (uint
 	return applied, shouldstop
 }
 
-// applyRequest interprets r as a call to store.X and returns a Response interpreted
-// from store.Event
-func (s *EtcdServer) applyRequest(r pb.Request) Response {
-	f := func(ev *store.Event, err error) Response {
-		return Response{Event: ev, err: err}
-	}
-
-	refresh, _ := pbutil.GetBool(r.Refresh)
-	ttlOptions := store.TTLOptionSet{Refresh: refresh}
-	if r.Expiration != 0 {
-		ttlOptions.ExpireTime = time.Unix(0, r.Expiration)
-	}
-
-	switch r.Method {
-	case "POST":
-		return f(s.store.Create(r.Path, r.Dir, r.Val, true, ttlOptions))
-	case "PUT":
-		exists, existsSet := pbutil.GetBool(r.PrevExist)
-		switch {
-		case existsSet:
-			if exists {
-				if r.PrevIndex == 0 && r.PrevValue == "" {
-					return f(s.store.Update(r.Path, r.Val, ttlOptions))
-				} else {
-					return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
-				}
-			}
-			return f(s.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions))
-		case r.PrevIndex > 0 || r.PrevValue != "":
-			return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
-		default:
-			if storeMemberAttributeRegexp.MatchString(r.Path) {
-				id := membership.MustParseMemberIDFromKey(path.Dir(r.Path))
-				var attr membership.Attributes
-				if err := json.Unmarshal([]byte(r.Val), &attr); err != nil {
-					plog.Panicf("unmarshal %s should never fail: %v", r.Val, err)
-				}
-				s.cluster.UpdateAttributes(id, attr)
-				// return an empty response since there is no consumer.
-				return Response{}
-			}
-			if r.Path == membership.StoreClusterVersionKey() {
-				s.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val)))
-				// return an empty response since there is no consumer.
-				return Response{}
-			}
-			return f(s.store.Set(r.Path, r.Dir, r.Val, ttlOptions))
-		}
-	case "DELETE":
-		switch {
-		case r.PrevIndex > 0 || r.PrevValue != "":
-			return f(s.store.CompareAndDelete(r.Path, r.PrevValue, r.PrevIndex))
+// applyEntryNormal apples an EntryNormal type raftpb request to the EtcdServer
+func (s *EtcdServer) applyEntryNormal(e *raftpb.Entry) {
+	// raft state machine may generate noop entry when leader confirmation.
+	// skip it in advance to avoid some potential bug in the future
+	if len(e.Data) == 0 {
+		select {
+		case s.forceVersionC <- struct{}{}:
 		default:
-			return f(s.store.Delete(r.Path, r.Dir, r.Recursive))
 		}
-	case "QGET":
-		return f(s.store.Get(r.Path, r.Recursive, r.Sorted))
-	case "SYNC":
-		s.store.DeleteExpiredKeys(time.Unix(0, r.Time))
-		return Response{}
-	default:
-		// This should never be reached, but just in case:
-		return Response{err: ErrUnknownMethod}
+		return
 	}
+
+	var raftReq pb.InternalRaftRequest
+	if !pbutil.MaybeUnmarshal(&raftReq, e.Data) { // backward compatible
+		var r pb.Request
+		pbutil.MustUnmarshal(&r, e.Data)
+		s.w.Trigger(r.ID, s.applyV2Request(&r))
+		return
+	}
+	if raftReq.V2 != nil {
+		req := raftReq.V2
+		s.w.Trigger(req.ID, s.applyV2Request(req))
+		return
+	}
+
+	// do not re-apply applied entries.
+	if e.Index <= s.consistIndex.ConsistentIndex() {
+		return
+	}
+	// set the consistent index of current executing entry
+	s.consistIndex.setConsistentIndex(e.Index)
+	ar := s.applyV3Request(&raftReq)
+	if ar.err != ErrNoSpace || len(s.alarmStore.Get(pb.AlarmType_NOSPACE)) > 0 {
+		s.w.Trigger(raftReq.ID, ar)
+		return
+	}
+
+	plog.Errorf("applying raft message exceeded backend quota")
+	go func() {
+		a := &pb.AlarmRequest{
+			MemberID: uint64(s.ID()),
+			Action:   pb.AlarmRequest_ACTIVATE,
+			Alarm:    pb.AlarmType_NOSPACE,
+		}
+		r := pb.InternalRaftRequest{Alarm: a}
+		s.processInternalRaftRequest(context.TODO(), r)
+		s.w.Trigger(raftReq.ID, ar)
+	}()
 }
 
 // applyConfChange applies a ConfChange to the server. It is only

+ 20 - 2
etcdserver/server_test.go

@@ -179,6 +179,7 @@ func TestApplyRepeat(t *testing.T) {
 		cluster:  cl,
 		reqIDGen: idutil.NewGenerator(0, time.Time{}),
 	}
+	s.applyV2 = &applierV2store{s}
 	s.start()
 	req := &pb.Request{Method: "QGET", ID: uint64(1)}
 	ents := []raftpb.Entry{{Index: 1, Data: pbutil.MustMarshal(req)}}
@@ -444,7 +445,8 @@ func TestApplyRequest(t *testing.T) {
 	for i, tt := range tests {
 		st := mockstore.NewRecorder()
 		srv := &EtcdServer{store: st}
-		resp := srv.applyRequest(tt.req)
+		srv.applyV2 = &applierV2store{srv}
+		resp := srv.applyV2Request(&tt.req)
 
 		if !reflect.DeepEqual(resp, tt.wresp) {
 			t.Errorf("#%d: resp = %+v, want %+v", i, resp, tt.wresp)
@@ -462,13 +464,15 @@ func TestApplyRequestOnAdminMemberAttributes(t *testing.T) {
 		store:   mockstore.NewRecorder(),
 		cluster: cl,
 	}
+	srv.applyV2 = &applierV2store{srv}
+
 	req := pb.Request{
 		Method: "PUT",
 		ID:     1,
 		Path:   membership.MemberAttributesStorePath(1),
 		Val:    `{"Name":"abc","ClientURLs":["http://127.0.0.1:2379"]}`,
 	}
-	srv.applyRequest(req)
+	srv.applyV2Request(&req)
 	w := membership.Attributes{Name: "abc", ClientURLs: []string{"http://127.0.0.1:2379"}}
 	if g := cl.Member(1).Attributes; !reflect.DeepEqual(g, w) {
 		t.Errorf("attributes = %v, want %v", g, w)
@@ -635,6 +639,7 @@ func TestDoProposal(t *testing.T) {
 			store:    st,
 			reqIDGen: idutil.NewGenerator(0, time.Time{}),
 		}
+		srv.applyV2 = &applierV2store{srv}
 		srv.start()
 		resp, err := srv.Do(context.Background(), tt)
 		srv.Stop()
@@ -661,6 +666,8 @@ func TestDoProposalCancelled(t *testing.T) {
 		w:        wt,
 		reqIDGen: idutil.NewGenerator(0, time.Time{}),
 	}
+	srv.applyV2 = &applierV2store{srv}
+
 	ctx, cancel := context.WithCancel(context.Background())
 	cancel()
 	_, err := srv.Do(ctx, pb.Request{Method: "PUT"})
@@ -681,6 +688,8 @@ func TestDoProposalTimeout(t *testing.T) {
 		w:        mockwait.NewNop(),
 		reqIDGen: idutil.NewGenerator(0, time.Time{}),
 	}
+	srv.applyV2 = &applierV2store{srv}
+
 	ctx, _ := context.WithTimeout(context.Background(), 0)
 	_, err := srv.Do(ctx, pb.Request{Method: "PUT"})
 	if err != ErrTimeout {
@@ -695,6 +704,8 @@ func TestDoProposalStopped(t *testing.T) {
 		w:        mockwait.NewNop(),
 		reqIDGen: idutil.NewGenerator(0, time.Time{}),
 	}
+	srv.applyV2 = &applierV2store{srv}
+
 	srv.done = make(chan struct{})
 	close(srv.done)
 	_, err := srv.Do(context.Background(), pb.Request{Method: "PUT", ID: 1})
@@ -710,6 +721,8 @@ func TestSync(t *testing.T) {
 		r:        raftNode{Node: n},
 		reqIDGen: idutil.NewGenerator(0, time.Time{}),
 	}
+	srv.applyV2 = &applierV2store{srv}
+
 	// check that sync is non-blocking
 	done := make(chan struct{})
 	go func() {
@@ -748,6 +761,8 @@ func TestSyncTimeout(t *testing.T) {
 		r:        raftNode{Node: n},
 		reqIDGen: idutil.NewGenerator(0, time.Time{}),
 	}
+	srv.applyV2 = &applierV2store{srv}
+
 	// check that sync is non-blocking
 	done := make(chan struct{})
 	go func() {
@@ -885,6 +900,8 @@ func TestTriggerSnap(t *testing.T) {
 		store:    st,
 		reqIDGen: idutil.NewGenerator(0, time.Time{}),
 	}
+	srv.applyV2 = &applierV2store{srv}
+
 	srv.kv = dstorage.New(be, &lease.FakeLessor{}, &srv.consistIndex)
 	srv.be = be
 
@@ -951,6 +968,7 @@ func TestConcurrentApplyAndSnapshotV3(t *testing.T) {
 		cluster:  cl,
 		msgSnapC: make(chan raftpb.Message, maxInFlightMsgSnap),
 	}
+	s.applyV2 = &applierV2store{s}
 
 	be, tmpPath := backend.NewDefaultTmpBackend()
 	defer func() {

+ 129 - 0
etcdserver/v2_server.go

@@ -0,0 +1,129 @@
+// Copyright 2016 CoreOS, 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 etcdserver
+
+import (
+	"time"
+
+	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
+	"golang.org/x/net/context"
+)
+
+type v2API interface {
+	Post(ctx context.Context, r *pb.Request) (Response, error)
+	Put(ctx context.Context, r *pb.Request) (Response, error)
+	Delete(ctx context.Context, r *pb.Request) (Response, error)
+	QGet(ctx context.Context, r *pb.Request) (Response, error)
+	Get(ctx context.Context, r *pb.Request) (Response, error)
+	Head(ctx context.Context, r *pb.Request) (Response, error)
+}
+
+type v2apiStore struct{ s *EtcdServer }
+
+func (a *v2apiStore) Post(ctx context.Context, r *pb.Request) (Response, error) {
+	return a.processRaftRequest(ctx, r)
+}
+
+func (a *v2apiStore) Put(ctx context.Context, r *pb.Request) (Response, error) {
+	return a.processRaftRequest(ctx, r)
+}
+
+func (a *v2apiStore) Delete(ctx context.Context, r *pb.Request) (Response, error) {
+	return a.processRaftRequest(ctx, r)
+}
+
+func (a *v2apiStore) QGet(ctx context.Context, r *pb.Request) (Response, error) {
+	return a.processRaftRequest(ctx, r)
+}
+
+func (a *v2apiStore) processRaftRequest(ctx context.Context, r *pb.Request) (Response, error) {
+	data, err := r.Marshal()
+	if err != nil {
+		return Response{}, err
+	}
+	ch := a.s.w.Register(r.ID)
+
+	// TODO: benchmark the cost of time.Now()
+	// might be sampling?
+	start := time.Now()
+	a.s.r.Propose(ctx, data)
+
+	proposePending.Inc()
+	defer proposePending.Dec()
+
+	select {
+	case x := <-ch:
+		proposeDurations.Observe(float64(time.Since(start)) / float64(time.Second))
+		resp := x.(Response)
+		return resp, resp.err
+	case <-ctx.Done():
+		proposeFailed.Inc()
+		a.s.w.Trigger(r.ID, nil) // GC wait
+		return Response{}, a.s.parseProposeCtxErr(ctx.Err(), start)
+	case <-a.s.done:
+	}
+	return Response{}, ErrStopped
+}
+
+func (a *v2apiStore) Get(ctx context.Context, r *pb.Request) (Response, error) {
+	if r.Wait {
+		wc, err := a.s.store.Watch(r.Path, r.Recursive, r.Stream, r.Since)
+		if err != nil {
+			return Response{}, err
+		}
+		return Response{Watcher: wc}, nil
+	}
+	ev, err := a.s.store.Get(r.Path, r.Recursive, r.Sorted)
+	if err != nil {
+		return Response{}, err
+	}
+	return Response{Event: ev}, nil
+}
+
+func (a *v2apiStore) Head(ctx context.Context, r *pb.Request) (Response, error) {
+	ev, err := a.s.store.Get(r.Path, r.Recursive, r.Sorted)
+	if err != nil {
+		return Response{}, err
+	}
+	return Response{Event: ev}, nil
+}
+
+// Do interprets r and performs an operation on s.store according to r.Method
+// and other fields. If r.Method is "POST", "PUT", "DELETE", or a "GET" with
+// Quorum == true, r will be sent through consensus before performing its
+// respective operation. Do will block until an action is performed or there is
+// an error.
+func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error) {
+	r.ID = s.reqIDGen.Next()
+	if r.Method == "GET" && r.Quorum {
+		r.Method = "QGET"
+	}
+	v2api := (v2API)(&v2apiStore{s})
+	switch r.Method {
+	case "POST":
+		return v2api.Post(ctx, &r)
+	case "PUT":
+		return v2api.Put(ctx, &r)
+	case "DELETE":
+		return v2api.Delete(ctx, &r)
+	case "QGET":
+		return v2api.QGet(ctx, &r)
+	case "GET":
+		return v2api.Get(ctx, &r)
+	case "HEAD":
+		return v2api.Head(ctx, &r)
+	}
+	return Response{}, ErrUnknownMethod
+}

+ 0 - 0
etcdserver/v3demo_server.go → etcdserver/v3_server.go