6.3.3 Remote Querying

Remote querying allows one QuLog process, the client, to query another, the server, providing both are connected to the same Pedro daemon. The server receives the query as a string inside a remote_query message term with sender identified as the client. If the server decides to answer the query using its knowledge base, it can do so by calling the respond_remote_query primitive. This parses the query string into a query term, locally evaluates the query, then returns answers to the client as a stream of answer bindings for query variables.

More precisely, to have a query Query evaluated remotely a special relation call is used of the form:

Query query_at ServerHandle

Here Query is the query to be remotely evaluated and ServerHandle is the Pedro registered handle of the server process where it should be evaluated.

The query has the same syntax as an interpreter query except it needs to be surrounded by brackets (unless it is a single condition). Examples are given in the QuLog user guide.

In the client program Query is a term. Before being sent to the server process it is analysed, translated into a string QueryString, and dispatched as a message term of the form:

remote_query(ID, QueryString)

where ID uniquely identifies the query for the client. As part of the translation to a string the variables whose bindings must be returned as an answer to the query are identified and linked with their required types. The server will respond by sending back a sequence of answers, each of which is paired with the query identifier ID. Each answer is a string representing a list of bindings for the identified answer variables. Each answer string is parsed by the client and type checked to ensure that the type of each answer variable binding corresponds to the client’s required type, and is ground. It then binds each of the answer variables of the client query term Query to its returned value.

The client waits up to a specified wait time for each answer. If any answer does not arrive within that time then the client behaves as though there were no more answers. So if no answer at all is received within the client’s default wait time the remote query call fails.

The default wait timeout (in seconds) is given by the system declared global variable remote_query_timeout. Like any global variable, the value can be accessed with

$remote_query_timeout

and changed with, for example,

remote_query_timeout := 1.5

The client ID sent in the remote query message term is only needed when the server is late in sending some answer and the client times out assuming there are no more answers. If the client sends another query, it will have a new identifier ID’. Now any late answers for prior remote queries from this client, not having the ID’ query identifier, are simply discarded by the remote query code.

On the server side, when the server calls

respond_remote_query(ID, QueryStr, Client)

the following takes place.

  1. The query string is parsed
  2. The parsed query is type checked
  3. The query is called and for each answer the server generates the required answer string and sends that back to the client.
  4. When there are no more solutions it sends back no_more_solutions

The query_at primitive handles receipt of the stream of answer bindings for the variables of the client. As an example consider the following example remote query:

(Person, Age :: age_of(Person, Age)) query_at server

The client first type checks the query and confirms that the answer variables will be ground after the call. The compiled call contains the answer type information:

Person : !human, Age: !age_val

The term,

remote_query(ID, "Person, Age :: age_of(Person, Age)")

where ID is the generated new message identifier, is then sent to the server identifying the sender of the message as the client thread in which the remote_query has been invoked This client thread then suspends waiting for a response, usually an ans message term, from the server.

Typically, the server responds to receiving this message by calling:

respond_remote_query(ID, "Person, Age :: age_of(Person, Age)", Client)

where Client is the handle of the client thread that called the remote query.

The query is then parsed and processed in the same way as if this was a query in the interpreter of the server except that a maximum of 20 answers will be found. To have more, or less answers returned, the maximum required number of answers may be specified using the N of ... query form. For example, the remote query:

(30 of Person, Age :: age_of(Person, Age)) query_at server

specifies that up to 30 answers should be returned if there are that many answers, or more.

(5 of Person, Age :: age_of(Person, Age)) query_at server

specifies at most 5 should be returned.

Suppose the server evaluatione of age_of(Person, Age) binds Person to roger and Age to 110. The server will send back the message

ans(ID, "[rvb_(Person, roger), rvb_(Age, 110)]")

The client reads the answer, checks it has the correct ID, parses the string, type checks the answer bindings against its saved types and accumulates the answers in a list until it gets the ans(ID, no_more_solutions) message. At this stage the different solutions for the original remote query

Query query_at ServerHandle

are generated, one at a time, by calling the list membership primitive with first argument the identified answer variables of Query and second argument the list of tuples of bindings for these variables constructed from the stream of answer strings received from the server.

The reader might be wondering why the query is turned into a string only to be parsed on the server and why the answers are likewise processed and why type checking is repeated on the server side and on the client side when the answer comes back.

Provided the client and the server use the same ontology (type definitions and declarations) then the query could be sent as a term rather than a string and the answers would automatically be of the correct type from the clients point of view. On the other hand, if the ontologies don’t align exactly then there are several possible problems with this approach:

  1. the term message sent by the client might not be a term on the server side and so the message would be rejected by the message reader;
  2. the dual problem is that an answer term might not be a term on the client side and so the answer message would be rejected;
  3. the query might type check on the client side but might not type check on the server side; or
  4. the answer terms might not have the required type (or not be ground) on the client side.

By converting back and forward to strings we guarantee that messages get through and by repeating type checking then we guarantee that we don’t get type problems.

If any of the above problems occur then the client code will raise a remote_query_ontology_mismatch exception.

For the reasons above we suggest that if you want to implement such a client-server you should put all relevant type definitions and declarations is a qulog file and consult that file in all clients and in the server. That will provide a shared ontology.

A server can be sent remote queries by many clients. The default is that every client has access to all the server defined relations. To restrict access to a public subset of the server’s relations, and even to specify different access capablities for different clients, we just need to include rules defining the system declared relation:

rel allowed_remote_query_from(??rel_term, !agent_handle)

in the server program. The handle argument identifies a client. Only queries from that client in which each relation call is inferably allowed using the server defined rules for allowed_remote_query_from will be evaluated by the server. If a received remote query contains any unallowed relation call the server evaluation of the query will fail. If the programmer wants to include remote querying in the allowed queries then this can be done using the dummy relation with declaration

rel remote_query_call(!handle)

So, for example,

allowed_remote_query_from(remote_query_call(server@localhost), _)

will allow forwarded remote queries from a client to server@localhost.

An example server program is given in the QuLog user guide.


On This Site