Commits

Adam Lindsay committed c9eac81

Diststore example now has key-value handover when adding/removing nodes. Some extra documentation.

Comments (0)

Files changed (7)

 as little as possible with node lookup. This is the simplest thing that
 I came up with that sort-of works: a flat peer-to-peer network with each
 node keeping up with the state of all the nodes as well as it can. This
-necessarily limits the ring's ability to scale beyond dozens of nodes.
+necessarily limits the ring's ability to scale beyond dozens of nodes.
+
+== diststore ==
+
+Diststore is an example application that can be layered atop the 
+thrifty-p2p location service. It's a toy distributed key-value store,
+like all the kids are doing these days. It makes no attempt to optimize
+for large scales, so don't get too excited about deploying this anywhere
+real. 
+
+Although diststore.thrift uses locator.thrift for a lot of its interfaces,
+the implementation overloads many of the base methods. Let's hope it ends
+up an informative example for either one of us. 
+
+Example usage, run this in a couple terminal windows:
+  python storeserver.py
+  
+Then in another window start populating the store:
+  python storeput.py a apple
+  python storeput.py b banana
+  python storeput.py c crayon
+  python storeput.py d dinosaur
+
+You can then query the store:
+  python storeget.py c
+  
+or start a new server, and see how a minimum of the existing keys 
+redistribute themselves:
+  python storeserver.py
+  
+or even stop a server with ctrl-C or a `kill -INT` signal and see 
+how the server hands its items off gracefully. However, the system 
+is not robust to any more aggressive termination: keys will go missing.
 
 include "locator.thrift"
 
-# struct StarterPackage {
-#  1: list<locator.Location> nodes,
-#  2: map<string, string>    store,
-# }
+struct StarterPackage {
+ 1: list<locator.Location> nodes,
+ 2: map<string, string>    store,
+}
 
 service Store extends locator.Locator {
- string         get     (1:string key)
- void           put     (1:string key, 2:string value)
-# StarterPackage join    (1:locator.Location location)
+ string                 get     (1:string key)
+ void                   put     (1:string key, 2:string value)
+ StarterPackage         join    (1:locator.Location location)
+ map<string, string>    add     (1:locator.Location location, 2:list<locator.Location> authorities)
 }

gen-py/diststore/Store-remote

   print 'Functions:'
   print '  string get(string key)'
   print '  void put(string key, string value)'
+  print '  StarterPackage join(Location location)'
+  print '   add(Location location,  authorities)'
   print ''
   sys.exit(0)
 
     sys.exit(1)
   pp.pprint(client.put(args[0],args[1],))
 
+elif cmd == 'join':
+  if len(args) != 1:
+    print 'join requires 1 args'
+    sys.exit(1)
+  pp.pprint(client.join(eval(args[0]),))
+
+elif cmd == 'add':
+  if len(args) != 2:
+    print 'add requires 2 args'
+    sys.exit(1)
+  pp.pprint(client.add(eval(args[0]),eval(args[1]),))
+
 transport.close()

gen-py/diststore/Store.py

     """
     pass
 
+  def join(self, location):
+    """
+    Parameters:
+     - location
+    """
+    pass
+
+  def add(self, location, authorities):
+    """
+    Parameters:
+     - location
+     - authorities
+    """
+    pass
+
 
 class Client(locator.Locator.Client, Iface):
   def __init__(self, iprot, oprot=None):
     self._iprot.readMessageEnd()
     return
 
+  def join(self, location):
+    """
+    Parameters:
+     - location
+    """
+    self.send_join(location)
+    return self.recv_join()
+
+  def send_join(self, location):
+    self._oprot.writeMessageBegin('join', TMessageType.CALL, self._seqid)
+    args = join_args()
+    args.location = location
+    args.write(self._oprot)
+    self._oprot.writeMessageEnd()
+    self._oprot.trans.flush()
+
+  def recv_join(self, ):
+    (fname, mtype, rseqid) = self._iprot.readMessageBegin()
+    if mtype == TMessageType.EXCEPTION:
+      x = TApplicationException()
+      x.read(self._iprot)
+      self._iprot.readMessageEnd()
+      raise x
+    result = join_result()
+    result.read(self._iprot)
+    self._iprot.readMessageEnd()
+    if result.success != None:
+      return result.success
+    raise TApplicationException(TApplicationException.MISSING_RESULT, "join failed: unknown result");
+
+  def add(self, location, authorities):
+    """
+    Parameters:
+     - location
+     - authorities
+    """
+    self.send_add(location, authorities)
+    return self.recv_add()
+
+  def send_add(self, location, authorities):
+    self._oprot.writeMessageBegin('add', TMessageType.CALL, self._seqid)
+    args = add_args()
+    args.location = location
+    args.authorities = authorities
+    args.write(self._oprot)
+    self._oprot.writeMessageEnd()
+    self._oprot.trans.flush()
+
+  def recv_add(self, ):
+    (fname, mtype, rseqid) = self._iprot.readMessageBegin()
+    if mtype == TMessageType.EXCEPTION:
+      x = TApplicationException()
+      x.read(self._iprot)
+      self._iprot.readMessageEnd()
+      raise x
+    result = add_result()
+    result.read(self._iprot)
+    self._iprot.readMessageEnd()
+    if result.success != None:
+      return result.success
+    raise TApplicationException(TApplicationException.MISSING_RESULT, "add failed: unknown result");
+
 
 class Processor(locator.Locator.Processor, Iface, TProcessor):
   def __init__(self, handler):
     locator.Locator.Processor.__init__(self, handler)
     self._processMap["get"] = Processor.process_get
     self._processMap["put"] = Processor.process_put
+    self._processMap["join"] = Processor.process_join
+    self._processMap["add"] = Processor.process_add
 
   def process(self, iprot, oprot):
     (name, type, seqid) = iprot.readMessageBegin()
     oprot.writeMessageEnd()
     oprot.trans.flush()
 
+  def process_join(self, seqid, iprot, oprot):
+    args = join_args()
+    args.read(iprot)
+    iprot.readMessageEnd()
+    result = join_result()
+    result.success = self._handler.join(args.location)
+    oprot.writeMessageBegin("join", TMessageType.REPLY, seqid)
+    result.write(oprot)
+    oprot.writeMessageEnd()
+    oprot.trans.flush()
+
+  def process_add(self, seqid, iprot, oprot):
+    args = add_args()
+    args.read(iprot)
+    iprot.readMessageEnd()
+    result = add_result()
+    result.success = self._handler.add(args.location, args.authorities)
+    oprot.writeMessageBegin("add", TMessageType.REPLY, seqid)
+    result.write(oprot)
+    oprot.writeMessageEnd()
+    oprot.trans.flush()
+
 
 # HELPER FUNCTIONS AND STRUCTURES
 
   def __ne__(self, other):
     return not (self == other)
 
+class join_args(object):
+  """
+  Attributes:
+   - location
+  """
 
+  thrift_spec = (
+    None, # 0
+    (1, TType.STRUCT, 'location', (locator.ttypes.Location, locator.ttypes.Location.thrift_spec), None, ), # 1
+  )
+
+  def __init__(self, location=None,):
+    self.location = location
+
+  def read(self, iprot):
+    if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None:
+      fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec))
+      return
+    iprot.readStructBegin()
+    while True:
+      (fname, ftype, fid) = iprot.readFieldBegin()
+      if ftype == TType.STOP:
+        break
+      if fid == 1:
+        if ftype == TType.STRUCT:
+          self.location = locator.ttypes.Location()
+          self.location.read(iprot)
+        else:
+          iprot.skip(ftype)
+      else:
+        iprot.skip(ftype)
+      iprot.readFieldEnd()
+    iprot.readStructEnd()
+
+  def write(self, oprot):
+    if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None:
+      oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec)))
+      return
+    oprot.writeStructBegin('join_args')
+    if self.location != None:
+      oprot.writeFieldBegin('location', TType.STRUCT, 1)
+      self.location.write(oprot)
+      oprot.writeFieldEnd()
+    oprot.writeFieldStop()
+    oprot.writeStructEnd()
+
+  def __repr__(self):
+    L = ['%s=%r' % (key, value)
+      for key, value in self.__dict__.iteritems()]
+    return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
+
+  def __eq__(self, other):
+    return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+  def __ne__(self, other):
+    return not (self == other)
+
+class join_result(object):
+  """
+  Attributes:
+   - success
+  """
+
+  thrift_spec = (
+    (0, TType.STRUCT, 'success', (StarterPackage, StarterPackage.thrift_spec), None, ), # 0
+  )
+
+  def __init__(self, success=None,):
+    self.success = success
+
+  def read(self, iprot):
+    if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None:
+      fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec))
+      return
+    iprot.readStructBegin()
+    while True:
+      (fname, ftype, fid) = iprot.readFieldBegin()
+      if ftype == TType.STOP:
+        break
+      if fid == 0:
+        if ftype == TType.STRUCT:
+          self.success = StarterPackage()
+          self.success.read(iprot)
+        else:
+          iprot.skip(ftype)
+      else:
+        iprot.skip(ftype)
+      iprot.readFieldEnd()
+    iprot.readStructEnd()
+
+  def write(self, oprot):
+    if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None:
+      oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec)))
+      return
+    oprot.writeStructBegin('join_result')
+    if self.success != None:
+      oprot.writeFieldBegin('success', TType.STRUCT, 0)
+      self.success.write(oprot)
+      oprot.writeFieldEnd()
+    oprot.writeFieldStop()
+    oprot.writeStructEnd()
+
+  def __repr__(self):
+    L = ['%s=%r' % (key, value)
+      for key, value in self.__dict__.iteritems()]
+    return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
+
+  def __eq__(self, other):
+    return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+  def __ne__(self, other):
+    return not (self == other)
+
+class add_args(object):
+  """
+  Attributes:
+   - location
+   - authorities
+  """
+
+  thrift_spec = (
+    None, # 0
+    (1, TType.STRUCT, 'location', (locator.ttypes.Location, locator.ttypes.Location.thrift_spec), None, ), # 1
+    (2, TType.LIST, 'authorities', (TType.STRUCT,(locator.ttypes.Location, locator.ttypes.Location.thrift_spec)), None, ), # 2
+  )
+
+  def __init__(self, location=None, authorities=None,):
+    self.location = location
+    self.authorities = authorities
+
+  def read(self, iprot):
+    if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None:
+      fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec))
+      return
+    iprot.readStructBegin()
+    while True:
+      (fname, ftype, fid) = iprot.readFieldBegin()
+      if ftype == TType.STOP:
+        break
+      if fid == 1:
+        if ftype == TType.STRUCT:
+          self.location = locator.ttypes.Location()
+          self.location.read(iprot)
+        else:
+          iprot.skip(ftype)
+      elif fid == 2:
+        if ftype == TType.LIST:
+          self.authorities = []
+          (_etype19, _size16) = iprot.readListBegin()
+          for _i20 in xrange(_size16):
+            _elem21 = locator.ttypes.Location()
+            _elem21.read(iprot)
+            self.authorities.append(_elem21)
+          iprot.readListEnd()
+        else:
+          iprot.skip(ftype)
+      else:
+        iprot.skip(ftype)
+      iprot.readFieldEnd()
+    iprot.readStructEnd()
+
+  def write(self, oprot):
+    if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None:
+      oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec)))
+      return
+    oprot.writeStructBegin('add_args')
+    if self.location != None:
+      oprot.writeFieldBegin('location', TType.STRUCT, 1)
+      self.location.write(oprot)
+      oprot.writeFieldEnd()
+    if self.authorities != None:
+      oprot.writeFieldBegin('authorities', TType.LIST, 2)
+      oprot.writeListBegin(TType.STRUCT, len(self.authorities))
+      for iter22 in self.authorities:
+        iter22.write(oprot)
+      oprot.writeListEnd()
+      oprot.writeFieldEnd()
+    oprot.writeFieldStop()
+    oprot.writeStructEnd()
+
+  def __repr__(self):
+    L = ['%s=%r' % (key, value)
+      for key, value in self.__dict__.iteritems()]
+    return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
+
+  def __eq__(self, other):
+    return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+  def __ne__(self, other):
+    return not (self == other)
+
+class add_result(object):
+  """
+  Attributes:
+   - success
+  """
+
+  thrift_spec = (
+    (0, TType.MAP, 'success', (TType.STRING,None,TType.STRING,None), None, ), # 0
+  )
+
+  def __init__(self, success=None,):
+    self.success = success
+
+  def read(self, iprot):
+    if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None:
+      fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec))
+      return
+    iprot.readStructBegin()
+    while True:
+      (fname, ftype, fid) = iprot.readFieldBegin()
+      if ftype == TType.STOP:
+        break
+      if fid == 0:
+        if ftype == TType.MAP:
+          self.success = {}
+          (_ktype24, _vtype25, _size23 ) = iprot.readMapBegin() 
+          for _i27 in xrange(_size23):
+            _key28 = iprot.readString();
+            _val29 = iprot.readString();
+            self.success[_key28] = _val29
+          iprot.readMapEnd()
+        else:
+          iprot.skip(ftype)
+      else:
+        iprot.skip(ftype)
+      iprot.readFieldEnd()
+    iprot.readStructEnd()
+
+  def write(self, oprot):
+    if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None:
+      oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec)))
+      return
+    oprot.writeStructBegin('add_result')
+    if self.success != None:
+      oprot.writeFieldBegin('success', TType.MAP, 0)
+      oprot.writeMapBegin(TType.STRING, TType.STRING, len(self.success))
+      for kiter30,viter31 in self.success.items():
+        oprot.writeString(kiter30)
+        oprot.writeString(viter31)
+      oprot.writeMapEnd()
+      oprot.writeFieldEnd()
+    oprot.writeFieldStop()
+    oprot.writeStructEnd()
+
+  def __repr__(self):
+    L = ['%s=%r' % (key, value)
+      for key, value in self.__dict__.iteritems()]
+    return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
+
+  def __eq__(self, other):
+    return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+  def __ne__(self, other):
+    return not (self == other)
+
+

gen-py/diststore/ttypes.py

   fastbinary = None
 
 
+class StarterPackage(object):
+  """
+  Attributes:
+   - nodes
+   - store
+  """
+
+  thrift_spec = (
+    None, # 0
+    (1, TType.LIST, 'nodes', (TType.STRUCT,(locator.ttypes.Location, locator.ttypes.Location.thrift_spec)), None, ), # 1
+    (2, TType.MAP, 'store', (TType.STRING,None,TType.STRING,None), None, ), # 2
+  )
+
+  def __init__(self, nodes=None, store=None,):
+    self.nodes = nodes
+    self.store = store
+
+  def read(self, iprot):
+    if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None:
+      fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec))
+      return
+    iprot.readStructBegin()
+    while True:
+      (fname, ftype, fid) = iprot.readFieldBegin()
+      if ftype == TType.STOP:
+        break
+      if fid == 1:
+        if ftype == TType.LIST:
+          self.nodes = []
+          (_etype3, _size0) = iprot.readListBegin()
+          for _i4 in xrange(_size0):
+            _elem5 = locator.ttypes.Location()
+            _elem5.read(iprot)
+            self.nodes.append(_elem5)
+          iprot.readListEnd()
+        else:
+          iprot.skip(ftype)
+      elif fid == 2:
+        if ftype == TType.MAP:
+          self.store = {}
+          (_ktype7, _vtype8, _size6 ) = iprot.readMapBegin() 
+          for _i10 in xrange(_size6):
+            _key11 = iprot.readString();
+            _val12 = iprot.readString();
+            self.store[_key11] = _val12
+          iprot.readMapEnd()
+        else:
+          iprot.skip(ftype)
+      else:
+        iprot.skip(ftype)
+      iprot.readFieldEnd()
+    iprot.readStructEnd()
+
+  def write(self, oprot):
+    if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None:
+      oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec)))
+      return
+    oprot.writeStructBegin('StarterPackage')
+    if self.nodes != None:
+      oprot.writeFieldBegin('nodes', TType.LIST, 1)
+      oprot.writeListBegin(TType.STRUCT, len(self.nodes))
+      for iter13 in self.nodes:
+        iter13.write(oprot)
+      oprot.writeListEnd()
+      oprot.writeFieldEnd()
+    if self.store != None:
+      oprot.writeFieldBegin('store', TType.MAP, 2)
+      oprot.writeMapBegin(TType.STRING, TType.STRING, len(self.store))
+      for kiter14,viter15 in self.store.items():
+        oprot.writeString(kiter14)
+        oprot.writeString(viter15)
+      oprot.writeMapEnd()
+      oprot.writeFieldEnd()
+    oprot.writeFieldStop()
+    oprot.writeStructEnd()
+
+  def __repr__(self):
+    L = ['%s=%r' % (key, value)
+      for key, value in self.__dict__.iteritems()]
+    return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
+
+  def __eq__(self, other):
+    return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+  def __ne__(self, other):
+    return not (self == other)
+
         return map(str2loc, self.ring.nodes)
     
     def get_node(self, key):
-        return str2loc(self.ring.get_node(key))
+        if self.ring.nodes:
+            return str2loc(self.ring.get_node(key))
+        else:
+            return Location('',0)
     
     def ping(self):
         print 'ping()'
 from thrift.server import TServer
 
 from locator.ttypes import Location
-#from locator import Locator
+from diststore import Store
+from diststore.ttypes import *
 import location
-from diststore import Store
 
 DEFAULTPORT = 9900
 
+usage = '''
+  python %s [[peer] port]
+
+Starts a distributed key-value storage node on the designated port
+and contacting the designated peer. In the absence of these two, it 
+attempts to autodiscover an open port and a peer on localhost, working
+from the default port, %d.
+
+After auto-joining, a node will receive key-value pairs from its 
+neighbors. When exiting cleanly (e.g., with a KeyboardInterrupt), the
+node hands off all its items to the appropriate neighbors.
+
+Usage can be obtained with -h or --help as the first argument.
+''' % (sys.argv[0], DEFAULTPORT)
+
+
 def remote_call(destination, method, *args):
     transport = TSocket.TSocket(destination.address, destination.port)
     transport = TTransport.TBufferedTransport(transport)
         'Make it quiet for the example'
         pass
     
-    # def add(self, loc, authorities):
-    #     location.LocatorHandler.add(self, loc, authorities)
-    #     locstr = location.loc2str(loc)
-    #     for key, value in self.store.items(): 
-    #         if location.loc2str(self.get_node(a)) == locstr:
-    #             try:
-    #                 remote_call(loc, 'put', key, value)
-    #             except location.NodeNotFound, tx:
-    #                 pass
+    def join(self, location):
+        """
+        Parameters:
+         - location
+        """
+        store = self.add(location, [self.location])
+        return StarterPackage(self.get_all(), store)
+    
+    def add(self, loc, authorities):
+        """
+        Parameters:
+         - location
+         - authorities
+        """
+        key = location.loc2str(loc)
+        store = dict()
+        self.addnews[key].add(self.here)
+        self.addnews[key].update(map(location.loc2str, authorities))
+        self.removenews[key] = set()
+        destinations = location.select_peers(self.ring.nodes.difference(self.addnews[key]))
+        self.addnews[key].update(destinations)        
+        for destination in destinations:
+            try:
+                store.update(remote_call(location.str2loc(destination), 
+                    'add', loc, map(location.str2loc, self.addnews[key])))
+            except location.NodeNotFound, tx:
+                self.remove(tx.location, map(location.str2loc, self.ring.nodes))
+        locstr = location.loc2str(loc)
+        self.ring.append(locstr)
+        for key, value in self.store.items():
+            if location.loc2str(self.get_node(key)) == locstr:
+                store.update([(key, value)])
+                del self.store[key] 
+                print 'dropped %s' % key
+        print "added %s:%d" % (loc.address, loc.port)
+        return store
     
     def debug(self):
         a = "self.location: %r\n" % self.location
         a += "self.store:\n%r\n" % self.store
         print a
     
+    def local_join(self):
+        if self.peer:
+            starter = remote_call(self.peer, 'join', self.location)
+            if starter.nodes:
+                self.ring.extend(map(location.loc2str, starter.nodes))
+            print 'Joining the network...'
+            self.store.update(starter.store)
+            for key in self.store.keys():
+                print "received %s" % key
+        else:
+            self.ring.append(self.here)
+            print 'Initiating the network...'
+    
     def cleanup(self):
         self.ring.remove(self.here)
         for dest in location.select_peers(self.ring.nodes):
             try:
                 remote_call(location.str2loc(dest), 'remove', self.location, [self.location])
             except location.NodeNotFound, tx:
-                self.ring.remove(loc2str(tx.location))            
+                self.ring.remove(location.loc2str(tx.location))            
         for key, value in ((a, b) for (a, b) in self.store.items() if b):
             dest = self.get_node(key)
             try:
                 pass
     
 
-#
 def main(inputargs):
     handler = StoreHandler(**inputargs)
     processor = Store.Processor(handler)
     server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
     
     handler.local_join()
-    
     print 'Starting the server at %s...' % (handler.here)
     try:
         server.serve()
 if __name__ == '__main__':
     inputargs = {}
     try:
+        if '-h' in sys.argv[1]:
+            print usage
+            sys.exit()
         inputargs['port'] = int(sys.argv[-1])
         inputargs['peer'] = location.str2loc(sys.argv[-2])
-    except:
+    except StandardError:
         pass
     if 'port' not in inputargs:
         loc = location.ping_until_not_found(Location('localhost', DEFAULTPORT), 25)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.