Задани на лабораторные работы. ПРК / Professional Microsoft Robotics Developer Studio
.pdf
www.it-ebooks.info
Chapter 3: Decentralized Software Services (DSS)
: base(body)
{
}
}
A basic Replace handler might look like the following:
[ServiceHandler(ServiceHandlerBehavior.Exclusive)] public IEnumerator<ITask> ReplaceHandler(Replace replace)
{
_state = replace.Body; replace.ResponsePort.Post(DefaultReplaceResponseType.Instance); yield break;
}
Notice that the handler is declared in the Exclusive group. This guarantees that the state is not updated by two different handlers at the same time, which could lead to an inconsistent state.
However, note also that the code does no checking of the incoming state. Most of the samples in the MRDS distribution are like this (if they have a Replace operation). It would be better to check crucial values in the state before making the replacement.
The handler sends an acknowledgment message to the ResponsePort when it has finished. It uses a pre-defined message called DefaultReplaceResponseType.Instance that makes your life easier because you don’t need to create a response type. (Similar instances are available for other types of operations; use IntelliSense to look for them.) If the partner service cannot continue until the replacement is complete, then it can wait for this response message to arrive.
Update
You use the Update messages when only a portion of the state needs to change. You need to define an appropriate class to hold all the information you want to update, and then write a handler to perform the update. This must be an Exclusive handler to avoid potential conflicts.
Defining a Class for an Update Request
Go back to ServiceBTypes.cs in Visual Studio. At the bottom of the file below the Get class, add the following code:
///<summary>
///ServiceB Set Interval Operation
///</summary>
public class SetInterval: Update<SetIntervalRequest, PortSet<DefaultUpdateResponseType,Fault>>
{
public SetInterval()
{
}
}
///<summary>
///Set Interval Request
(continued)
133
www.it-ebooks.info
Part I: Robotics Developer Studio Fundamentals
(continued)
/// </summary> [DataContract] [DataMemberConstructor]
public class SetIntervalRequest
{
[DataMember, DataMemberConstructor] public int Interval;
}
The new class, SetInterval, is based on Update and uses a request message type of
SetIntervalRequest and returns a DefaultUpdateResponseType if it is successful.
The SetIntervalRequest class has only one member, which is the Interval. Notice that there is a data contract on SetIntervalRequest and it requests that DssProxy generate a helper to construct a new instance using the [DataMemberConstructor] attribute. Because of the constructor, you can execute this operation from another service using the following shorthand:
_servicebPort.SetInterval(1500);
This assumes, of course, that _servicebPort is a service forwarder port that points to ServiceB.
Adding a Handler to Process Update Requests
Now you need to go to ServiceB.cs to add the handler for the SetInterval operation. At the bottom of the file, add the following handler just before the end of the service class:
///<summary>
///Set Interval Handler
///</summary>
///<param name=”request”>SetIntervalRequest</param>
///<returns></returns> [ServiceHandler(ServiceHandlerBehavior.Exclusive)]
public virtual IEnumerator<ITask> SetIntervalHandler(SetInterval request)
{
if (_state == null)
// Oops! Return a failure request.ResponsePort.Post(new Fault());
else
{
// Set the interval
_state.Interval = request.Body.Interval; // Return a success response
request.ResponsePort.Post(DefaultUpdateResponseType.Instance);
}
yield break;
}
This handler simply updates the _state.Interval from the request body and then sends back an acknowledgment using one of the pre-defined default response types.
134
www.it-ebooks.info
Chapter 3: Decentralized Software Services (DSS)
It is worth mentioning at this point that this example demonstrates defensive programming: The code checks for a null state before using it. When services start, there is no guarantee in what order they will complete their initialization. A race condition is possible if another service sends a request before the initial state of ServiceB has been loaded. ServiceB must protect itself from an access violation by confirming that the state actually exists before updating it.
Note that the ServiceB GetHandler should also be modified to determine whether the state is null:
///<summary>
///Get Handler
///</summary>
///<param name=”get”></param>
///<returns></returns> [ServiceHandler(ServiceHandlerBehavior.Concurrent)] public virtual IEnumerator<ITask> GetHandler(Get get)
{
if (_state == null)
//Oops! Return a failure
//This can happen due to race conditions if another service
//issues a Get before the Initial State has been defined
get.ResponsePort.Post(new Fault());
else
// Return the state get.ResponsePort.Post(_state);
yield break;
}
Returning Errors
Notice in the preceding code that if there is no state, a Fault is returned. Errors are always returned this way via the response port. However, the code just creates an empty Fault object. This is not good practice, and the programmer should be reprimanded. A Fault should always include information about what caused the error.
There are several ways to create a Fault that contains useful information. Inside the catch block
of a try/catch, you can use the Fault.FromException helper method to convert the Exception into a Fault.
If you want to use a string as the error message (called the reason), you can use the following:
// Create a new Fault based on the error message
Fault fault = Fault.FromCodeSubcodeReason(FaultCodes.Receiver, DsspFaultCodes.OperationFailed, “Some error message”);
There are many different fault codes and DSSP fault codes to choose from. You can list them in the editor by using IntelliSense and find the ones that are most appropriate for your situation.
135
www.it-ebooks.info
Part I: Robotics Developer Studio Fundamentals
Service Initialization
You have already seen the Start method that must be present in every service. This is where you place your initialization code. When a new service is created, it includes a call to base.Start, which does the following:
Calls ActivateDsspOperationHandlers on all your main and alternate ports, trying to hook up handlers with the [ServiceHandler]attribute to ports in the operation PortSets
Sends a DirectoryInsert to add your service to the Service Directory
Does a LogInfo with the URI of the service
If you have a lot of initialization to do, don’t call base.Start until after you have completed the initialization. This appears to be counter to the comment that is automatically inserted when you create a service, but it does not cause any problems:
protected override void Start()
{
base.Start();
// Add service specific initialization here.
}
In older code (from V1.0) you might see the following pattern instead of base.Start:
//Listen for each operation type and call its Service Handler ActivateDsspOperationHandlers();
//Publish the service to the local Node Directory DirectoryInsert();
//Display HTTP service Uri
LogInfo(LogGroups.Console, “Service uri: “);
Going back even further to older code, ActivateDsspOperationHandlers was not used; there was an explicit declaration of the main interleave — for example, if you look in ArcosCore.cs for the Pioneer 3DX robot:
Activate(Arbiter.Interleave( new TeardownReceiverGroup
(
Arbiter.Receive<DsspDefaultDrop>(false,_mainPort,DropHandler)
),
new ExclusiveReceiverGroup
(
Arbiter.Receive<Replace>(true, _mainPort, ReplaceHandler), Arbiter.ReceiveWithIterator<Subscribe>(true, _mainPort, SubscribeHandler),
),
new ConcurrentReceiverGroup
(
Arbiter.Receive<DsspDefaultLookup>(true,_mainPort,DefaultLookupHandler),
136
www.it-ebooks.info
Chapter 3: Decentralized Software Services (DSS)
Arbiter.Receive<Get>(true, _mainPort, GetHandler), Arbiter.Receive<HttpGet>(true,_mainPort, HttpGetHandler), Arbiter.Receive<HttpQuery>(true,_mainPort, HttpQueryHandler), Arbiter.Receive<Query>(true, _mainPort, QueryHandler), Arbiter.Receive<Update>(true, _mainPort, UpdateHandler),
)
));
You no longer have to do all of this because DSS uses reflection to find your handlers based on the [ServiceHandler]attribute and the method signature. However, it is helpful to understand what is actually happening when your service starts.
Note that if you want to add handlers to the main interleave after the service has started, you should use the Arbiter.CombineWith method. If you create a new interleave, it runs independently of the main interleave and therefore does not guarantee exclusivity. Likewise, if you use SpawnIterator or Activate to start a task, then the task runs outside of the main interleave.
A common thread in the discussion forums is about how to introduce delays during initialization to ensure that other services have started up properly. Using Thread.Sleep is not recommended because it blocks a thread. If possible, check the status of other services by sending a Get request to them (and waiting for the response) or looking in the directory to see whether they are visible.
Another alternative is to use SpawnIterator to start a new thread running an iterator. Then you can use the TimeoutPort to insert delays (if you really feel that you need to), which will release the thread until the timeout occurs. With Arbiter.ExecuteToCompletion, you can even call a series of other iterators, one after another.
In summary, best practice for initialization is to use iterators, and avoid blocking threads.
Composing and Coordinating Services
One of the benefits of writing everything as services is that you can combine, or orchestrate, services to create more complex applications. This section discusses how to start and stop services and establish partnerships.
Starting and Stopping Services Programmatically
In most cases, you do not need to explicitly start or stop a service because all the services you require will be started when the manifest is loaded. However, this section explains the procedure. The next section (“Using the Partner Attribute”) shows you an easier approach.
Remember that ServiceA is the one that is in charge. It partners with ServiceB and makes requests to ServiceB. Therefore, open ServiceA.cs in Visual Studio.
137
www.it-ebooks.info
Part I: Robotics Developer Studio Fundamentals
Adding a Service Forwarder Port
You need a service forwarder port so that you can send requests to ServiceB. However, because the code will create a new instance of ServiceB, the service port must be null initially. Add the following code somewhere near the top of the service class:
//Create a port to access Service B,
//but we don’t know where to send messages yet serviceb.ServiceBOperations _servicebPort = null;
For the moment, ignore the question of how to establish a partnership between ServiceA and ServiceB. In the Start method of ServiceA, add a line of code to spawn a new task:
/// </summary>
protected override void Start()
{
base.Start();
//Add service specific initialization here. Console.WriteLine(“ServiceA starting”);
//Start the main task on a separate thread and return SpawnIterator(MainTask);
}
This achieves the purpose of leaving a thread running but finishing the service initialization as far as DSS is concerned.
Main ServiceA Behavior
Underneath the Start method, add the MainTask. This routine creates a new ServiceB; calls MainLoop, which runs until it is satisfied; and then closes down the services and the DSS node:
private IEnumerator<ITask> MainTask()
{
Port<EmptyValue> done = new Port<EmptyValue>();
SpawnIterator<Port<EmptyValue>>(done, CreatePartner);
//Wait for a message to say that ServiceB is up and running yield return Arbiter.Receive(
false,
done, EmptyHandler
);
//Check that we have a forwarder
if (_servicebPort == null)
{
LogError(“There is no ServiceB”); yield break;
}
else
138
www.it-ebooks.info
Chapter 3: Decentralized Software Services (DSS)
{
SpawnIterator<Port<EmptyValue>>(done, MainLoop); yield return Arbiter.Receive(
false,
done, EmptyHandler
);
}
//We no longer require ServiceB so shut it down
//NOTE: This is not necessary -- it is just here to illustrate that
//other services can be shut down
LogInfo(LogGroups.Console, “Dropping ServiceB ...”); _servicebPort.DsspDefaultDrop();
Console.WriteLine(“ServiceA finished”); // Wait a while for ServiceB to exit yield return Arbiter.Receive(
false,
TimeoutPort(500), delegate(DateTime time) { }
);
//Pause for user input --
//This is just so that the DSS node stays up for the time being Console.WriteLine(“Press Enter to exit:”);
Console.ReadLine();
//Shut down the DSS node
ControlPanelPort.Post( 
new Microsoft.Dss.Services.ControlPanel.DropProcess());
yield break;
}
Using Completion Ports for Synchronization
As an example, MainTask creates a port called done for signaling purposes. Because no information needs to be transferred, it uses the EmptyValue class. Then it spawns the CreatePartner iterator and waits on the done port. An alternative, and much neater way to do it without signaling on a port, is to use Arbiter.ExecuteToCompletion.
The first step in CreatePartner is to call CreateService using the ServiceB contract identifier. If this is successful, then a new URI is created based on the service information. Then a ServiceForwarder is created. This enables messages to be sent to the operations port of the newly created service. (The _servicebPort was declared at the beginning of this section as a global).
private IEnumerator<ITask> CreatePartner(Port<EmptyValue> p)
{
// Create ServiceB instance Console.WriteLine(“Creating new ServiceB”);
yield return Arbiter.Choice(CreateService(serviceb.Contract.
Identifier),
delegate(CreateResponse s)
(continued)
139
www.it-ebooks.info
Part I: Robotics Developer Studio Fundamentals
(continued)
{
// Create Request succeeded.
LogInfo(LogGroups.Console, “ServiceB created: “ + s.Service);
Uri addr; try
{
//Create URI from service instance string addr = new Uri(s.Service);
//Create forwarder to ServiceB
_servicebPort =
ServiceForwarder<serviceb.ServiceBOperations>(addr);
}
catch (Exception ex)
{
LogError(LogGroups.Console,
“Could not create forwarder: “ + ex.Message);
}
},
delegate(W3C.Soap.Fault failure)
{
// Request failed
LogError(LogGroups.Console, “Could not start ServiceB”);
}
);
// Signal that the create is finished p.Post(EmptyValue.SharedInstance);
yield break;
}
Assuming that a service forwarder port is created successfully, the MainLoop iterator is called (also using the done port to signify completion). MainLoop looks like this:
private IEnumerator<ITask> MainLoop(Port<EmptyValue> p)
{
// Issue several Gets to ServiceB
yield return Arbiter.ExecuteToCompletion(Environment.TaskQueue, Arbiter.FromIteratorHandler(GetData));
//Change the update interval on ServiceB _servicebPort.SetInterval(1500);
//Do some more Gets to see the effect of the changed interval yield return Arbiter.ExecuteToCompletion(Environment.TaskQueue,
Arbiter.FromIteratorHandler(GetData));
//Finally, post a message to say that we are finished p.Post(EmptyValue.SharedInstance);
yield break;
}
140
www.it-ebooks.info
Chapter 3: Decentralized Software Services (DSS)
MainLoop in turn calls another iterator to request the sensor data from ServiceB. Then it changes the update interval in ServiceB and again reads the sensor data several times using GetData:
// Request “sensor” data from ServiceB private IEnumerator<ITask> GetData()
{
for (int i = 0; i < 10; i++)
{
// Send a Get request to ServiceB
yield return Arbiter.Choice(_servicebPort.Get(), delegate(serviceb.ServiceBState s)
{
// Get request succeeded LogInfo(LogGroups.Console, 
“ServiceB Sensor: “ + s.SensorValue); },
delegate(W3C.Soap.Fault failure)
{
// Get request failed LogError(LogGroups.Console, 
“Get to ServiceB failed” + failure.Reason);
}
);
// Wait for 1 second
yield return Arbiter.Receive( false, TimeoutPort(1000), delegate(DateTime time) { }
);
}
yield break;
}
This code uses the LogInfo method rather than Console.WriteLine. The output is therefore clearly differentiated from the Console.WriteLine output, as you can see in Figure 3-22.
Notice that GetData uses Get operations on ServiceB, repeated at one-second intervals. This timer interval does not change, but partway through, ServiceA changes the interval in ServiceB. You should be able to see the effects of these mismatched time intervals quite clearly in the output. This merely illustrates that sometimes the data you get might be a little stale.
Dropping Services
When the MainLoop is finished, a Drop message is sent to ServiceB (from MainTask). Then the code waits for the user to press Enter. This is not something that you would normally do. In particular, Console.ReadLine is not CCR-friendly and should not be used. However, it enables you to poke around inside the DSS node using a web browser before the whole thing is closed down.
141
www.it-ebooks.info
Part I: Robotics Developer Studio Fundamentals
To stop a service, you send it a Drop message. By default, the DSS runtime handles Drop requests for you, so you don’t have to write any code. However, ServiceB does something a little nasty — it runs an infinite loop on a dedicated thread. All sorts of “nasties” can arise when you are programming MRDS services, so it is worthwhile to review Drop handlers here.
Go to the bottom of the ServiceB.cs file in Visual Studio. (If you have been following the instructions, there should be a SetIntervalHandler there). Insert the following code:
///<summary>
///Drop Handler
///</summary>
///<param name=”drop”></param>
///<returns></returns> [ServiceHandler(ServiceHandlerBehavior.Teardown)]
public virtual IEnumerator<ITask> DropHandler(DsspDefaultDrop drop)
{
// Tell the main loop to stop _shutdown = true;
Console.WriteLine(“ServiceB shutting down”);
//Make sure you do this or the sender might be blocked
//waiting for a response. The base handler will send
//a response for us.
base.DefaultDropHandler(drop);
// That’s all folks! yield break;
}
Make sure that the Drop handler is in the TearDown group. If you inadvertently place it into the Concurrent group, very strange things happen. The service will disappear from the service directory, but it continues executing!
The only function that the DropHandler needs to perform is to set the _shutdown flag so that the main loop terminates on the next iteration. Then it calls base.DefaultDropHandler to finish the job.
Just declaring this handler is sufficient to override the default handler defined in DSS — there is nothing else you need to do to make it work.
Now ServiceA can send a message to ServiceB to tell it to terminate itself:
_servicebPort.DsspDefaultDrop();
The last step is for ServiceA to send a DropProcess message to the Control Panel, which shuts down the DSS node.
142
