2015-06-01 15:49:10 +00:00
|
|
|
package nomad
|
|
|
|
|
2015-06-03 10:26:50 +00:00
|
|
|
import (
|
2017-08-31 00:45:32 +00:00
|
|
|
"context"
|
2015-06-03 10:26:50 +00:00
|
|
|
"crypto/tls"
|
2015-06-05 22:22:05 +00:00
|
|
|
"fmt"
|
2015-06-03 10:26:50 +00:00
|
|
|
"io"
|
2015-06-07 18:50:53 +00:00
|
|
|
"math/rand"
|
2015-06-03 10:26:50 +00:00
|
|
|
"net"
|
2015-11-16 02:27:02 +00:00
|
|
|
"net/rpc"
|
2015-06-03 10:26:50 +00:00
|
|
|
"strings"
|
2015-06-05 22:22:05 +00:00
|
|
|
"time"
|
2015-06-03 10:26:50 +00:00
|
|
|
|
|
|
|
"github.com/armon/go-metrics"
|
2016-05-03 07:15:29 +00:00
|
|
|
"github.com/hashicorp/consul/lib"
|
2017-02-05 20:03:11 +00:00
|
|
|
memdb "github.com/hashicorp/go-memdb"
|
2015-06-03 10:26:50 +00:00
|
|
|
"github.com/hashicorp/net-rpc-msgpackrpc"
|
2015-08-23 02:17:49 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/state"
|
2015-06-05 22:22:05 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2015-10-11 21:39:34 +00:00
|
|
|
"github.com/hashicorp/raft"
|
2015-06-03 10:26:50 +00:00
|
|
|
"github.com/hashicorp/yamux"
|
|
|
|
)
|
|
|
|
|
2015-06-01 15:49:10 +00:00
|
|
|
type RPCType byte
|
|
|
|
|
|
|
|
const (
|
2015-06-03 10:26:50 +00:00
|
|
|
rpcNomad RPCType = 0x01
|
|
|
|
rpcRaft = 0x02
|
|
|
|
rpcMultiplex = 0x03
|
|
|
|
rpcTLS = 0x04
|
|
|
|
)
|
|
|
|
|
2015-06-05 22:22:05 +00:00
|
|
|
const (
|
2015-06-07 18:50:53 +00:00
|
|
|
// maxQueryTime is used to bound the limit of a blocking query
|
|
|
|
maxQueryTime = 300 * time.Second
|
|
|
|
|
|
|
|
// defaultQueryTime is the amount of time we block waiting for a change
|
|
|
|
// if no time is specified. Previously we would wait the maxQueryTime.
|
|
|
|
defaultQueryTime = 300 * time.Second
|
|
|
|
|
|
|
|
// jitterFraction is a the limit to the amount of jitter we apply
|
|
|
|
// to a user specified MaxQueryTime. We divide the specified time by
|
2016-07-10 17:36:55 +00:00
|
|
|
// the fraction. So 16 == 6.25% limit of jitter. This jitter is also
|
|
|
|
// applied to RPCHoldTimeout.
|
2015-06-07 18:50:53 +00:00
|
|
|
jitterFraction = 16
|
|
|
|
|
2015-06-05 22:22:05 +00:00
|
|
|
// Warn if the Raft command is larger than this.
|
|
|
|
// If it's over 1MB something is probably being abusive.
|
|
|
|
raftWarnSize = 1024 * 1024
|
|
|
|
|
|
|
|
// enqueueLimit caps how long we will wait to enqueue
|
|
|
|
// a new Raft command. Something is probably wrong if this
|
|
|
|
// value is ever reached. However, it prevents us from blocking
|
|
|
|
// the requesting goroutine forever.
|
|
|
|
enqueueLimit = 30 * time.Second
|
|
|
|
)
|
|
|
|
|
2015-11-16 02:27:02 +00:00
|
|
|
// NewClientCodec returns a new rpc.ClientCodec to be used to make RPC calls to
|
|
|
|
// the Nomad Server.
|
|
|
|
func NewClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec {
|
2016-02-21 02:05:17 +00:00
|
|
|
return msgpackrpc.NewCodecFromHandle(true, true, conn, structs.HashiMsgpackHandle)
|
2015-11-16 02:27:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewServerCodec returns a new rpc.ServerCodec to be used by the Nomad Server
|
|
|
|
// to handle rpcs.
|
|
|
|
func NewServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec {
|
2016-02-21 02:05:17 +00:00
|
|
|
return msgpackrpc.NewCodecFromHandle(true, true, conn, structs.HashiMsgpackHandle)
|
2015-11-16 02:27:02 +00:00
|
|
|
}
|
|
|
|
|
2015-06-03 10:26:50 +00:00
|
|
|
// listen is used to listen for incoming RPC connections
|
|
|
|
func (s *Server) listen() {
|
|
|
|
for {
|
|
|
|
// Accept a connection
|
|
|
|
conn, err := s.rpcListener.Accept()
|
|
|
|
if err != nil {
|
|
|
|
if s.shutdown {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
s.logger.Printf("[ERR] nomad.rpc: failed to accept RPC conn: %v", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
go s.handleConn(conn, false)
|
|
|
|
metrics.IncrCounter([]string{"nomad", "rpc", "accept_conn"}, 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleConn is used to determine if this is a Raft or
|
|
|
|
// Nomad type RPC connection and invoke the correct handler
|
|
|
|
func (s *Server) handleConn(conn net.Conn, isTLS bool) {
|
|
|
|
// Read a single byte
|
|
|
|
buf := make([]byte, 1)
|
|
|
|
if _, err := conn.Read(buf); err != nil {
|
|
|
|
if err != io.EOF {
|
|
|
|
s.logger.Printf("[ERR] nomad.rpc: failed to read byte: %v", err)
|
|
|
|
}
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-06 03:50:35 +00:00
|
|
|
// Enforce TLS if EnableRPC is set
|
|
|
|
if s.config.TLSConfig.EnableRPC && !isTLS && RPCType(buf[0]) != rpcTLS {
|
2015-06-03 10:26:50 +00:00
|
|
|
s.logger.Printf("[WARN] nomad.rpc: Non-TLS connection attempted with RequireTLS set")
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Switch on the byte
|
|
|
|
switch RPCType(buf[0]) {
|
|
|
|
case rpcNomad:
|
|
|
|
s.handleNomadConn(conn)
|
|
|
|
|
|
|
|
case rpcRaft:
|
|
|
|
metrics.IncrCounter([]string{"nomad", "rpc", "raft_handoff"}, 1)
|
|
|
|
s.raftLayer.Handoff(conn)
|
|
|
|
|
|
|
|
case rpcMultiplex:
|
|
|
|
s.handleMultiplex(conn)
|
|
|
|
|
|
|
|
case rpcTLS:
|
|
|
|
if s.rpcTLS == nil {
|
|
|
|
s.logger.Printf("[WARN] nomad.rpc: TLS connection attempted, server not configured for TLS")
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
conn = tls.Server(conn, s.rpcTLS)
|
|
|
|
s.handleConn(conn, true)
|
|
|
|
|
|
|
|
default:
|
|
|
|
s.logger.Printf("[ERR] nomad.rpc: unrecognized RPC byte: %v", buf[0])
|
|
|
|
conn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleMultiplex is used to multiplex a single incoming connection
|
|
|
|
// using the Yamux multiplexer
|
|
|
|
func (s *Server) handleMultiplex(conn net.Conn) {
|
|
|
|
defer conn.Close()
|
|
|
|
conf := yamux.DefaultConfig()
|
|
|
|
conf.LogOutput = s.config.LogOutput
|
|
|
|
server, _ := yamux.Server(conn, conf)
|
|
|
|
for {
|
|
|
|
sub, err := server.Accept()
|
|
|
|
if err != nil {
|
|
|
|
if err != io.EOF {
|
|
|
|
s.logger.Printf("[ERR] nomad.rpc: multiplex conn accept failed: %v", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
go s.handleNomadConn(sub)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleNomadConn is used to service a single Nomad RPC connection
|
|
|
|
func (s *Server) handleNomadConn(conn net.Conn) {
|
|
|
|
defer conn.Close()
|
2015-11-16 02:27:02 +00:00
|
|
|
rpcCodec := NewServerCodec(conn)
|
2015-06-03 10:26:50 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-s.shutdownCh:
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.rpcServer.ServeRequest(rpcCodec); err != nil {
|
|
|
|
if err != io.EOF && !strings.Contains(err.Error(), "closed") {
|
|
|
|
s.logger.Printf("[ERR] nomad.rpc: RPC error: %v (%v)", err, conn)
|
|
|
|
metrics.IncrCounter([]string{"nomad", "rpc", "request_error"}, 1)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
metrics.IncrCounter([]string{"nomad", "rpc", "request"}, 1)
|
|
|
|
}
|
|
|
|
}
|
2015-06-05 22:22:05 +00:00
|
|
|
|
2015-06-07 18:50:53 +00:00
|
|
|
// forward is used to forward to a remote region or to forward to the local leader
|
|
|
|
// Returns a bool of if forwarding was performed, as well as any error
|
|
|
|
func (s *Server) forward(method string, info structs.RPCInfo, args interface{}, reply interface{}) (bool, error) {
|
2016-07-10 17:36:55 +00:00
|
|
|
var firstCheck time.Time
|
|
|
|
|
2015-06-07 18:50:53 +00:00
|
|
|
region := info.RequestRegion()
|
2015-07-06 20:18:12 +00:00
|
|
|
if region == "" {
|
|
|
|
return true, fmt.Errorf("missing target RPC")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle region forwarding
|
2015-06-07 18:50:53 +00:00
|
|
|
if region != s.config.Region {
|
2015-06-07 19:08:47 +00:00
|
|
|
err := s.forwardRegion(region, method, args, reply)
|
2015-06-07 18:50:53 +00:00
|
|
|
return true, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we can allow a stale read
|
|
|
|
if info.IsRead() && info.AllowStaleRead() {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2016-07-10 17:36:55 +00:00
|
|
|
CHECK_LEADER:
|
|
|
|
// Find the leader
|
|
|
|
isLeader, remoteServer := s.getLeader()
|
|
|
|
|
|
|
|
// Handle the case we are the leader
|
|
|
|
if isLeader {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle the case of a known leader
|
|
|
|
if remoteServer != nil {
|
|
|
|
err := s.forwardLeader(remoteServer, method, args, reply)
|
2015-06-07 18:50:53 +00:00
|
|
|
return true, err
|
|
|
|
}
|
2016-07-10 17:36:55 +00:00
|
|
|
|
|
|
|
// Gate the request until there is a leader
|
|
|
|
if firstCheck.IsZero() {
|
|
|
|
firstCheck = time.Now()
|
|
|
|
}
|
|
|
|
if time.Now().Sub(firstCheck) < s.config.RPCHoldTimeout {
|
|
|
|
jitter := lib.RandomStagger(s.config.RPCHoldTimeout / jitterFraction)
|
|
|
|
select {
|
|
|
|
case <-time.After(jitter):
|
|
|
|
goto CHECK_LEADER
|
|
|
|
case <-s.shutdownCh:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No leader found and hold time exceeded
|
|
|
|
return true, structs.ErrNoLeader
|
2015-06-07 18:50:53 +00:00
|
|
|
}
|
|
|
|
|
2016-07-10 17:36:55 +00:00
|
|
|
// getLeader returns if the current node is the leader, and if not
|
|
|
|
// then it returns the leader which is potentially nil if the cluster
|
|
|
|
// has not yet elected a leader.
|
|
|
|
func (s *Server) getLeader() (bool, *serverParts) {
|
|
|
|
// Check if we are the leader
|
|
|
|
if s.IsLeader() {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2015-06-07 18:50:53 +00:00
|
|
|
// Get the leader
|
|
|
|
leader := s.raft.Leader()
|
|
|
|
if leader == "" {
|
2016-07-10 17:36:55 +00:00
|
|
|
return false, nil
|
2015-06-07 18:50:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lookup the server
|
|
|
|
s.peerLock.RLock()
|
|
|
|
server := s.localPeers[leader]
|
|
|
|
s.peerLock.RUnlock()
|
|
|
|
|
2016-07-10 17:36:55 +00:00
|
|
|
// Server could be nil
|
|
|
|
return false, server
|
|
|
|
}
|
|
|
|
|
|
|
|
// forwardLeader is used to forward an RPC call to the leader, or fail if no leader
|
|
|
|
func (s *Server) forwardLeader(server *serverParts, method string, args interface{}, reply interface{}) error {
|
2015-06-07 18:50:53 +00:00
|
|
|
// Handle a missing server
|
|
|
|
if server == nil {
|
|
|
|
return structs.ErrNoLeader
|
|
|
|
}
|
2016-05-28 01:14:34 +00:00
|
|
|
return s.connPool.RPC(s.config.Region, server.Addr, server.MajorVersion, method, args, reply)
|
2015-06-07 18:50:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// forwardRegion is used to forward an RPC call to a remote region, or fail if no servers
|
2015-06-07 19:08:47 +00:00
|
|
|
func (s *Server) forwardRegion(region, method string, args interface{}, reply interface{}) error {
|
2015-06-07 18:50:53 +00:00
|
|
|
// Bail if we can't find any servers
|
|
|
|
s.peerLock.RLock()
|
|
|
|
servers := s.peers[region]
|
|
|
|
if len(servers) == 0 {
|
|
|
|
s.peerLock.RUnlock()
|
|
|
|
s.logger.Printf("[WARN] nomad.rpc: RPC request for region '%s', no path found",
|
|
|
|
region)
|
|
|
|
return structs.ErrNoRegionPath
|
|
|
|
}
|
|
|
|
|
|
|
|
// Select a random addr
|
2016-05-03 07:29:23 +00:00
|
|
|
offset := rand.Intn(len(servers))
|
2015-06-07 18:50:53 +00:00
|
|
|
server := servers[offset]
|
|
|
|
s.peerLock.RUnlock()
|
|
|
|
|
|
|
|
// Forward to remote Nomad
|
|
|
|
metrics.IncrCounter([]string{"nomad", "rpc", "cross-region", region}, 1)
|
2016-05-28 01:14:34 +00:00
|
|
|
return s.connPool.RPC(region, server.Addr, server.MajorVersion, method, args, reply)
|
2015-06-07 18:50:53 +00:00
|
|
|
}
|
|
|
|
|
2015-10-11 21:39:34 +00:00
|
|
|
// raftApplyFuture is used to encode a message, run it through raft, and return the Raft future.
|
|
|
|
func (s *Server) raftApplyFuture(t structs.MessageType, msg interface{}) (raft.ApplyFuture, error) {
|
2016-02-21 01:36:39 +00:00
|
|
|
buf, err := structs.Encode(t, msg)
|
2015-06-05 22:22:05 +00:00
|
|
|
if err != nil {
|
2015-10-11 21:39:34 +00:00
|
|
|
return nil, fmt.Errorf("Failed to encode request: %v", err)
|
2015-06-05 22:22:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Warn if the command is very large
|
|
|
|
if n := len(buf); n > raftWarnSize {
|
|
|
|
s.logger.Printf("[WARN] nomad: Attempting to apply large raft entry (type %d) (%d bytes)", t, n)
|
|
|
|
}
|
|
|
|
|
|
|
|
future := s.raft.Apply(buf, enqueueLimit)
|
2015-10-11 21:39:34 +00:00
|
|
|
return future, nil
|
|
|
|
}
|
2015-06-05 22:22:05 +00:00
|
|
|
|
2017-06-28 22:35:52 +00:00
|
|
|
// raftApplyFn is the function signature for applying a msg to Raft
|
|
|
|
type raftApplyFn func(t structs.MessageType, msg interface{}) (interface{}, uint64, error)
|
|
|
|
|
2015-10-11 21:39:34 +00:00
|
|
|
// raftApply is used to encode a message, run it through raft, and return
|
|
|
|
// the FSM response along with any errors
|
|
|
|
func (s *Server) raftApply(t structs.MessageType, msg interface{}) (interface{}, uint64, error) {
|
|
|
|
future, err := s.raftApplyFuture(t, msg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
2015-10-11 21:42:21 +00:00
|
|
|
if err := future.Error(); err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
2015-07-06 20:34:32 +00:00
|
|
|
return future.Response(), future.Index(), nil
|
2015-06-05 22:22:05 +00:00
|
|
|
}
|
2015-07-06 21:23:15 +00:00
|
|
|
|
|
|
|
// setQueryMeta is used to populate the QueryMeta data for an RPC call
|
|
|
|
func (s *Server) setQueryMeta(m *structs.QueryMeta) {
|
|
|
|
if s.IsLeader() {
|
|
|
|
m.LastContact = 0
|
|
|
|
m.KnownLeader = true
|
|
|
|
} else {
|
|
|
|
m.LastContact = time.Now().Sub(s.raft.LastContact())
|
|
|
|
m.KnownLeader = (s.raft.Leader() != "")
|
|
|
|
}
|
|
|
|
}
|
2015-08-23 02:17:49 +00:00
|
|
|
|
2017-02-05 20:03:11 +00:00
|
|
|
// queryFn is used to perform a query operation. If a re-query is needed, the
|
|
|
|
// passed-in watch set will be used to block for changes. The passed-in state
|
|
|
|
// store should be used (vs. calling fsm.State()) since the given state store
|
|
|
|
// will be correctly watched for changes if the state store is restored from
|
|
|
|
// a snapshot.
|
|
|
|
type queryFn func(memdb.WatchSet, *state.StateStore) error
|
|
|
|
|
2015-08-23 02:17:49 +00:00
|
|
|
// blockingOptions is used to parameterize blockingRPC
|
|
|
|
type blockingOptions struct {
|
2015-10-29 21:47:39 +00:00
|
|
|
queryOpts *structs.QueryOptions
|
|
|
|
queryMeta *structs.QueryMeta
|
2017-02-05 20:03:11 +00:00
|
|
|
run queryFn
|
2015-08-23 02:17:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// blockingRPC is used for queries that need to wait for a
|
|
|
|
// minimum index. This is used to block and wait for changes.
|
|
|
|
func (s *Server) blockingRPC(opts *blockingOptions) error {
|
2017-08-31 00:45:32 +00:00
|
|
|
var deadline time.Time
|
|
|
|
ctx := context.Background()
|
|
|
|
var cancel context.CancelFunc
|
2015-08-23 02:17:49 +00:00
|
|
|
var state *state.StateStore
|
|
|
|
|
|
|
|
// Fast path non-blocking
|
|
|
|
if opts.queryOpts.MinQueryIndex == 0 {
|
|
|
|
goto RUN_QUERY
|
|
|
|
}
|
|
|
|
|
|
|
|
// Restrict the max query time, and ensure there is always one
|
|
|
|
if opts.queryOpts.MaxQueryTime > maxQueryTime {
|
|
|
|
opts.queryOpts.MaxQueryTime = maxQueryTime
|
|
|
|
} else if opts.queryOpts.MaxQueryTime <= 0 {
|
|
|
|
opts.queryOpts.MaxQueryTime = defaultQueryTime
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply a small amount of jitter to the request
|
2016-05-03 07:15:29 +00:00
|
|
|
opts.queryOpts.MaxQueryTime += lib.RandomStagger(opts.queryOpts.MaxQueryTime / jitterFraction)
|
2015-08-23 02:17:49 +00:00
|
|
|
|
|
|
|
// Setup a query timeout
|
2017-08-31 00:45:32 +00:00
|
|
|
deadline = time.Now().Add(opts.queryOpts.MaxQueryTime)
|
|
|
|
ctx, cancel = context.WithDeadline(context.Background(), deadline)
|
|
|
|
defer cancel()
|
2015-08-23 02:17:49 +00:00
|
|
|
|
|
|
|
RUN_QUERY:
|
|
|
|
// Update the query meta data
|
|
|
|
s.setQueryMeta(opts.queryMeta)
|
|
|
|
|
2017-02-05 20:03:11 +00:00
|
|
|
// Increment the rpc query counter
|
2015-08-23 02:17:49 +00:00
|
|
|
metrics.IncrCounter([]string{"nomad", "rpc", "query"}, 1)
|
2017-02-05 20:03:11 +00:00
|
|
|
|
2017-02-08 19:18:03 +00:00
|
|
|
// We capture the state store and its abandon channel but pass a snapshot to
|
|
|
|
// the blocking query function. We operate on the snapshot to allow separate
|
|
|
|
// calls to the state store not all wrapped within the same transaction.
|
2017-02-05 20:03:11 +00:00
|
|
|
state = s.fsm.State()
|
2017-02-08 19:18:03 +00:00
|
|
|
abandonCh := state.AbandonCh()
|
2017-02-08 06:10:33 +00:00
|
|
|
snap, _ := state.Snapshot()
|
2017-02-08 19:18:03 +00:00
|
|
|
stateSnap := &snap.StateStore
|
2017-02-05 20:03:11 +00:00
|
|
|
|
|
|
|
// We can skip all watch tracking if this isn't a blocking query.
|
|
|
|
var ws memdb.WatchSet
|
2017-02-08 04:31:23 +00:00
|
|
|
if opts.queryOpts.MinQueryIndex > 0 {
|
2017-02-05 20:03:11 +00:00
|
|
|
ws = memdb.NewWatchSet()
|
|
|
|
|
|
|
|
// This channel will be closed if a snapshot is restored and the
|
|
|
|
// whole state store is abandoned.
|
2017-02-08 19:18:03 +00:00
|
|
|
ws.Add(abandonCh)
|
2017-02-05 20:03:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Block up to the timeout if we didn't see anything fresh.
|
2017-02-08 19:18:03 +00:00
|
|
|
err := opts.run(ws, stateSnap)
|
2015-08-23 02:17:49 +00:00
|
|
|
|
|
|
|
// Check for minimum query time
|
2015-10-27 22:52:40 +00:00
|
|
|
if err == nil && opts.queryOpts.MinQueryIndex > 0 && opts.queryMeta.Index <= opts.queryOpts.MinQueryIndex {
|
2017-09-01 16:53:09 +00:00
|
|
|
if err := ws.WatchCtx(ctx); err == nil {
|
2017-02-05 20:03:11 +00:00
|
|
|
goto RUN_QUERY
|
2015-08-23 02:17:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|