Class: Servicy::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/server/server.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(transport = nil, load_balancer = nil, logger_stream = nil, config_file = nil) ⇒ Server

Create a new server that is used to register and find Services. TODO: I should really make this take an options hash, but that will require changing a lot of shit…

Parameters:

  • transport ({Servicy::Transport}) (defaults to: nil)

    The transport used to send and receive messages.



11
12
13
14
15
16
17
18
# File 'lib/server/server.rb', line 11

def initialize(transport=nil, load_balancer=nil, logger_stream=nil, config_file=nil)
  @transport            = transport || Servicy.config.server.transport
  @services             = [] # All services
  @deactivated_services = [] # Dead services
  @load_balancer        = load_balancer || Servicy.config.server.load_balancer
  @config_file          = config_file   || Servicy.config.server.config_file
  start_heartbeat_process
end

Instance Attribute Details

#deactivated_servicesObject (readonly)

Returns the value of attribute deactivated_services.



3
4
5
# File 'lib/server/server.rb', line 3

def deactivated_services
  @deactivated_services
end

#load_balancerObject

Returns the value of attribute load_balancer.



4
5
6
# File 'lib/server/server.rb', line 4

def load_balancer
  @load_balancer
end

#servicesObject (readonly)

Returns the value of attribute services.



3
4
5
# File 'lib/server/server.rb', line 3

def services
  @services
end

#transportObject

Returns the value of attribute transport.



4
5
6
# File 'lib/server/server.rb', line 4

def transport
  @transport
end

Class Method Details

.load(filename) ⇒ Servicy::Server

Loads a server configuration off disk

Parameters:

  • filename (String)

    The path of the file to load

Returns:



70
71
72
73
74
# File 'lib/server/server.rb', line 70

def self.load(filename)
  s = Servicy::Server.new
  s.load!(filename)
  s
end

Instance Method Details

#find(args = {}) ⇒ Object

Find one or more services based on some search criteria. When finding things, you can search by any single or combination of name, version, and api definition. When searching for name:

find(name: 'com.foobar.baz')

… or for wild-cards if you don’t care who provides a thing

find(name: '.baz')

For searching by version, you can search by absolute version number, minimum, maximum, or a range (by passing min and max together):

# Absolute version
find(version: '1.2.4-p123')

# Min and max as range
find(min_version: '1.2.0', max_version: '2.0.0')

All version numbers must follow the format:

(major).(minor).(revision)(-patch)?

And are compared to one another by converting the version to numbers and comparing those numbers. The version happens by:

(major * 1000) + (minor * 100) + (revision * 10) + (1/patch)

Searching by api can be used to search for a service that provides a certain interface, even if you don’t know what that interface is called. For example, if you don’t know what your authorization service is called, but you know that it takes 2 strings (username and password) and returns either false or a User object, you can search by:

find(api: '.login/2[String,String] -> User|nil')

NOTE The query syntax listed here isn’t working currently. See Servicy::ServiceSearcher for information on how to actually search by api in the current version.



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/server/server.rb', line 201

def find(args={})
  Servicy.logger.info "Finding #{args.inspect}..."

  searcher = Servicy::ServiceSearcher.new(services - deactivated_services)
  searcher.filter_by_name args[:name]
  searcher.filter_by_versions args[:min_version], args[:max_version], args[:version]
  searcher.filter_by_api args[:api]

  Servicy.logger.info "Found #{searcher.services.length} providers for #{args.inspect}"
  searcher.services
end

#find_one(args = {}) ⇒ Object

Find one or more services based on some search criteria. When finding things, you can search by any single or combination of name, version, and api definition. When searching for name:

find(name: 'com.foobar.baz')

… or for wild-cards if you don’t care who provides a thing

find(name: '.baz')

For searching by version, you can search by absolute version number, minimum, maximum, or a range (by passing min and max together):

# Absolute version
find(version: '1.2.4-p123')

# Min and max as range
find(min_version: '1.2.0', max_version: '2.0.0')

All version numbers must follow the format:

(major).(minor).(revision)(-patch)?

And are compared to one another by converting the version to numbers and comparing those numbers. The version happens by:

(major * 1000) + (minor * 100) + (revision * 10) + (1/patch)

Searching by api can be used to search for a service that provides a certain interface, even if you don’t know what that interface is called. For example, if you don’t know what your authorization service is called, but you know that it takes 2 strings (username and password) and returns either false or a User object, you can search by:

find(api: '.login/2[String,String] -> User|nil')

NOTE The query syntax listed here isn’t working currently. See Servicy::ServiceSearcher for information on how to actually search by api in the current version. Return only the first load-balanced instance. If you want load-balancing, use this.



216
217
218
# File 'lib/server/server.rb', line 216

def find_one(args={})
  load_balancer.next(find(args))
end

#go!Object

Starts the Transport listening (whatever that means for the implementation), gets messages, handles them, and sends replies.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/server/server.rb', line 22

def go!
  transport.start do |message|
    Servicy.logger.info "Received message, #{message.inspect}"
    last_service_count = @services.length

    result = case message.message
    when 'registration'
      self.register message.service
      Transport::Message.success
    when 'query'
      if message.only_one
        temp = [self.find_one(message.query)].compact
      else
        temp = self.find(message.query)
      end
      Transport::Message.services temp
    when 'stats'
      # Gather stats about the running system, and return it
      # TODO: Other stats?
      Transport::Message.statistics(num_services: last_service_count,
                                             uptimes: uptimes,
                                             latencies: latencies
                                            )
    else
      Transport::Message.error("Bad message type, #{message.inspect}")
    end

    if @services.length > last_service_count
      Servicy.logger.debug("Saving to #{@config_file}")
      self.save!(@config_file)
      Servicy.logger.debug("Saved")
      last_service_count = @services.length
    end
    result
  end

  transport.stop
end

#latenciesHash{String => Float}

Get a hash of uptimes for each service, where the key is the service name and hostname, joined with a ‘#’, and the value is the uptime for that service on that host. Hash is in the same format, but the values are avg latencies.

Returns:

  • (Hash{String => Float})

    the uptimes of all services per host.



230
231
232
# File 'lib/server/server.rb', line 230

def latencies
  services.inject({}) { |h, s| h[s.to_s] = s.avg_latency; h }
end

#load!(filename) ⇒ Object

Load a configuration from disk, and override the current one with it.

Parameters:

  • filename (String)

    The path to the configuration file.



148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/server/server.rb', line 148

def load!(filename)
  json = ''
  File.open(filename, 'r') do |f|
    json = f.read
  end

  @config_file = filename

  @services = JSON.parse(json).map do |s|
    s = s.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
    Servicy::Service.new s
  end
end

#register(args) ⇒ Object

Register a new service

:name is the name of the service you are registering, in
inverted-domain format (ie, "com.foo.api").
:host must be either an IP address or hostname/domain name that is
reachable by the Server instance (and any API consumers, of course.)
:port must be an integer between 1 and 65535, as with :heartbeat_port,
and is the port used to connect to the service provider.
:heartbeat_port is the port to connect to to make sure that things are
still running, and will default to the same as :port.
:version is the version of the API/service that you are registering,
and must be in the "major.minor.revision-patch" format, where the patch
section is optional. For example, "1.0.3-p124". It defaults to,
"1.0.0".
:protocol can be anything you like, but is the protocol used to connect
to the api. It defaults to "HTTP/S", meaning either HTTP or HTTP/TLS
:api can be an array of hashes that describe the API provided. See the
tests and README.md for examples.

Parameters:

  • args (Hash{Symbol => String,Fixnum,Array<Hash>})

    The arguments for the service to register. This must include:

    :name
    :host
    :port
    

    You can also provide optional data,

    :version
    :heartbeat_port
    :protocol
    :api
    


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/server/server.rb', line 103

def register(args)
  service = Service.new(args)
  Servicy.logger.info "Registering service, #{service}..."
  if service.up?
    if @services.include? service
      Servicy.logger.info "Service, #{service}, already exists. Not duplicating"
    else
      @services << service
      Servicy.logger.info "Service, #{service}, up; registered."
    end
    return service
  else
    Servicy.logger.info "Service, #{service}, down; not registered."
    return false
  end
end

#reregister(service) ⇒ Object

Removes a service from the deactivated list if it’s there. No side-effects otherwise.

Parameters:

  • service ({Service})

    The service to remove from deactivated list



132
133
134
135
136
# File 'lib/server/server.rb', line 132

def reregister(service)
  if @deactivated_services.delete(service)
    Servicy.logger.info "Re-registering service, #{service}"
  end
end

#save!(filename) ⇒ Object

Save the service configuration to disk

Parameters:

  • filename (String)

    The path to where the config should be saved



140
141
142
143
144
# File 'lib/server/server.rb', line 140

def save!(filename)
  File.open(filename, 'w') do |f|
    f.write services.map(&:as_json).to_json
  end
end

#stop!Object

Shut the server down



62
63
64
65
# File 'lib/server/server.rb', line 62

def stop!
  transport.stop
  @heartbeat_thread.kill
end

#unregister(service) ⇒ Object

Un-register a previously registered service. This removes it from the active list of services that are available to searches, etc.

Parameters:

  • service ({Service})

    the service to de-register



123
124
125
126
127
# File 'lib/server/server.rb', line 123

def unregister(service)
  return if @deactivated_services.include?(service)
  Servicy.logger.info "De-registering service, #{service}"
  @deactivated_services << service
end

#uptimesHash{String => Float}

Get a hash of uptimes for each service, where the key is the service name and hostname, joined with a ‘#’, and the value is the uptime for that service on that host.

Returns:

  • (Hash{String => Float})

    the uptimes of all services per host.



224
225
226
# File 'lib/server/server.rb', line 224

def uptimes
  services.inject({}) { |h, s| h[s.to_s] = s.uptime; h }
end