I l@ve RuBoard Next Section

• | Table of Contents
JavaSpaces™ in Practice
By Philip Bishop, Nigel Warren
Publisher | : Addison Wesley
Pub Date | : September 20, 2002
ISBN | : 0-321-11231-8
Pages | : 256

"Phil Bishop and Nigel Warren have been pioneers in the use of JavaSpaces(TM) to build real systems that solve real problems. This book is an excellent distillation of their accumulated wisdom on this subject, and will save any programmer using the technology far more time than it will take to read. It is also well written and clearly presented, a rarity in technical books these days. I learned a lot reading it."

-Jim Waldo, Distinguished Engineer at Sun Microsystems, lead architect for the Jini networking technology and JavaSpaces.

This book will show you how to use JavaSpaces to build practical, scalable distributed systems. It will increase your understanding of where JavaSpaces are applicable, and how to use them effectively within your system architecture.

JavaSpaces(TM) in Practice is structured in three parts:

  • Part One focuses on patterns for designing and evolving entries from the basics to advanced distributed data structures.

  • Part Two covers designs and idioms associated with remote events, code mobility, and both transient and persistent spaces.

  • Part Three highlights some practical applications including implementing smart proxies, location-based services and agent systems, together with guidelines and tips for testing and tuning JavaSpaces.

Developers and programmers with some experience in Java will learn by example a set of idioms that make distributed systems more dynamic and flexible.

I l@ve RuBoard Next Section


I l@ve RuBoard

• | Table of Contents
JavaSpaces™ in Practice
By Philip Bishop, Nigel Warren
Publisher | : Addison Wesley
Pub Date | : September 20, 2002
ISBN | : 0-321-11231-8
Pages | : 256

Copyright
Acknowledgments
Preface
 | Chapter summary
Chapter 1. Introduction
 | Foundations and terms
 | Distribution using spaces
 | The JavaSpaces interface
 | The structure of spaces
 | Summary
Part I: Entries
 | Chapter 2. Designing entries
 |  | Background
 |  | An initial Entry
 |  | Entry Wrapping
 |  | Interfaces for entries
 |  | Factories for entries
 |  | Design alternative
 |  | Entries for existing frameworks
 |  | The payload idiom
 |  | Summary
 | Chapter 3. Evolving entries
 |  | Maintaining compatibility
 |  | Standard techniques and data loss
 |  | Using inheritance to add fields
 |  | Removing fields from entries
 |  | Evolving nested key fields
 |  | An alternative to nested key fields
 |  | Problems with entry evolution
 |  | Summary
 | Chapter 4. Simple distributed data structures
 |  | Simple data structures
 |  | Ordered data structures
 |  | Iterating
 |  | Summary
 | Chapter 5. Hierarchical distributed data structures
 |  | Introduction
 |  | Organizational structures
 |  | Complex matching
 |  | Generic distributed trees
 |  | Summary
Part II: Mechanics
 | Chapter 6. Codebases
 |  | Basic requirements
 |  | The codebase system property
 |  | Summary
 | Chapter 7. Remote event-based idioms
 |  | Introducing remote events
 |  | The handback event handler
 |  | Object interactions
 |  | Adding event handlers at runtime
 |  | Leases
 |  | Event registration
 |  | Sequence numbers
 |  | Transactions
 |  | Leases and transactions
 |  | Using an Event Mailbox
 |  | Reliability
 |  | Summary
 | Chapter 8. Code mobility –behavior-based idioms
 |  | Introducing code mobility
 |  | What goes on behind the scenes?
 |  | Introducing behavior-based idioms
 |  | Building an employee system
 |  | Using subclassing to modify behavior at runtime
 |  | Using classloaders to modify behavior at runtime
 |  | Security and mobile code
 |  | Summary
 | Chapter 9. Using transient and persistent spaces
 |  | Transient spaces
 |  | Persistent spaces
 |  | Accessing different spaces
 |  | Specifying storage models with entries
 |  | Mixing snapshots
 |  | Registering the mixed proxy
 |  | Summary
Part III: Application
 | Chapter 10. Process coordination
 |  | Request–response model
 |  | Implementing the model
 |  | Coordinating multiple services
 |  | Summary
 | Chapter 11. Location-based services
 |  | Location-based systems
 |  | Packit Shifters – case study
 |  | Finding locatables in a space
 |  | Summary
 | Chapter 12. Bidding and agent systems
 |  | A time bidding system
 |  | Typesafe constants with spaces
 |  | Agents and spaces
 |  | The customer agent
 |  | The driver agents
 |  | Letter counter service
 |  | Summary
 | Chapter 13. Testing space-based systems
 |  | A space logging system
 |  | A local Logging Proxy
 |  | Reviewing the design
 |  | The JavaSpace logging service
 |  | Alternative designs
 |  | Summary
 | Chapter 14. Tuning space-based systems
 |  | A lightweight counter
 |  | Tuning local code
 |  | Using snapshot
 |  | Imitating snapshot
 |  | General entry optimizations
 |  | Date matching
 |  | Time and spaces
 |  | Server tuning
 |  | Summary
 |  | Conclusion
Appendix A. Code listings
 | ServiceLocator
 | SpaceLocator
 | TxnManager
 | Debug
 | ArrayMetaData
 | ExpandableArray
 | SpaceSafeConstant
 | Queue
 | QueuedEventListener
Appendix B. Tips and rules in this book
References
I l@ve RuBoard


Copyright

Pearson Education Limited

Head Office:

Edinburgh Gate

Harlow CM20 2JE

Tel: +44 (0)1279 623623

Fax: +44 (0)1279 431059

Website: www.it-minds.com

www.aw.com/cseng/

London Office:

128 Long Acre

London WC2E 9AN

Tel: +44 (0)20 7447 2000

Fax: +44 (0)20 7447 2170

First published in Great Britain in 2003

English edition © Pearson Education Ltd 2003

The rights of Phil Bishop and Nigel Warren to be identified as Authors of this Work have been asserted by them in accordance with the Copyright, Designs and Patents Act 1988.

British Library Cataloguing in Publication Data

A CIP catalogue record for this book can be obtained from the British Library.

Library of Congress Cataloging in Publication Data

Bishop, Phil, 1960-

Javaspaces in practice / Phil Bishop and Nigel Warren.

p. cm.

Includes bibliographical references and index

1. Java (Computer program language) 2. JavaSpaces technology. 3. Electronic data processing--Distributed processing. I. Warren, Nigel, 1965- II. Title.

QA76.73.J38 B56 2002

005.13'3--dc21

2002027079

All rights reserved; no part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise without either the prior written permission of the Publishers or a licence permitting restricted copying in the United Kingdom issued by the Copyright Licensing Agency Ltd, 90 Tottenham Court Road, London W1P 0LP. This book may not be lent, resold, hired out or otherwise disposed of by way of trade in any form of binding or cover other than that in which it is published, without the prior consent of the Publishers.

The programs in this book have been included for their instructional value. The publisher does not offer any warranties or representations in respect of their fitness for a particular purpose, nor does the publisher accept any liability for any loss or damage arising from their use.

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Pearson Education Limited has made every attempt to supply trademark information about manufacturers and their products mentioned in this book. A list of trademark designations and their owners appears on this page.

Trademark Notice

Java™, JavaMail™, JavaSpaces™, JDBC™, Jini™ and RMI™ are registered trademarks of Sun Microsystems.

10 9 8 7 6 5 4 3 2 1

Typeset by Pantek Arts Ltd, Maidstone, Kent.

Printed and bound in the UK by Biddles Ltd of Guildford and King's Lynn.

The Publishers' policy is to use paper manufactured from sustainable forests.


Acknowledgments

Special thanks go to our technical reviewers Dan Creswell at Intamission and Derek Baum at Paremus, who provided us with invaluable feedback on the early drafts of the material in this book.

We would also like to thank the following people who in various ways contributed to the writing of this book:

Neil Scanlon at pageweb.co.uk;

Maz Khan and Mike Clifton at Deutsche Bank (UK);

Ray O'Brien at E1 Works Ltd;

Ben Hutchison and Gordon Sim on the IntaMission platform team;

Jugdeep Singh, Amanda Huggett and Mark Pettit at IntaMission Consulting, and the whole of the IntaMission team;

Steve Osbourne and Andy Blackledge from Lost Wax for their help on the auction and bidding protocols;

Eric and Elizabeth Freeman;

Jim Waldo, John McClain and the Jini Team at Sun;

Simon Plumtree, Karen Sellwood, Katherin Ekstrom and the Addison-Wesley team;

Andy Mulholland, John Strassner, Wedge Green, Michael Ogg, Robert Branton, Greg Cutler, Mazen Traboulsi, Bill Venners, Robert Sander and Richard Summers for various notes and provocations.

Phil especially thanks Stella for missing out on a social life and putting up with all the early mornings and late nights while this book was being written. He would also like to thank Bella, Fifi, Coco, Carmen and Picasso who have provided hours of endless amusement and distractions!

Nigel especially thanks Mandy for being so tolerant and kind in the midst of the creative stresses and strains.


Preface

This is a book about how to use JavaSpaces to build practical distributed systems. Over the last few years we have helped clients and friends to build space-based systems. In this book we have tried to record the information and practices that we found useful when going about this task.

During this time it has been a delight to discover that Spaces inherently solve many of the problems traditionally associated with building scalable distributed systems. That is not to say that they are a panacea, but when we look back at other methods we have used over the past couple of decades to build and connect distributed software systems, it now seems impossible to consider building a "real" distributed system without a JavaSpace. In fact this is the same kind of reaction we had to Java when it was first launched.

Of course, Spaces are not appropriate for all systems. Sometimes, for example, a simple http server will suffice, but as systems become more complex JavaSpaces provide an escape route from the traditional issues of partial failure and ever-increasing complexity. In fact, one of the aims of this book is to increase your understanding of where Spaces are applicable and how to use Spaces effectively within your system's architectures.

Our goal when we started writing this book was to avoid repeating too much work covered in other books on this subject, most notably work in JavaSpaces™ Principles, Patterns and Practice by Freeman et al. (1999).

So as this book is not a beginner's guide to using or understanding JavaSpaces we would strongly recommend that you get your hands on a copy of that book if you find that some of the terminology or concepts here are not explicitly explained.

This book is grouped into three parts:

  • entries;

  • mechanics;

  • application.

Part I on entries, covers entry design, evolution, and distributed data structure design.

Part II covers designs and idioms associated with remote events, code mobility, and combining the use of both transient and persistent space.

Part III deals with the practical design and application of space-based systems.


Chapter summary

Part 1: Entries

The first four chapters of this part cover designing entries and data structures and some of the forces that influence their design. There is plenty more on designing space-based systems in the other sections too, but they build on some of the principles discussed in this first section.

Chapter 1: Introduction

In the first chapter we are going to take a look at JavaSpaces in general, starting with some foundation principles and outlining the terms that we intend to use throughout the book. We will also be taking a brief peek "under the hood" to see how Spaces are built, and the differences between transient and persistent Spaces.

Chapter 2: Designing entries

In the first chapter in the entries section we take a look at designing entries. We start off with a simple entry and discuss the rules governing its design. With this under our belt, we then go on to look into the issues relating to encapsulation, and describe various idioms that can be used to help hide the public nature of entries – these include Entry Wrapping, Interfaces, Factories, and how to design entries to work with existing interfaces. The chapter concludes by looking at the payload idiom, which can be used to reduce the indexing burden on a JavaSpace.

Chapter 3: Evolving entries

The second chapter in this section discusses the implications of evolving entry classes to satisfy new requirements as systems change over time. Initially we investigate the standard Java technique used for maintaining class compatibility and how it can result in data loss under some circumstances. We then discuss why the standard technique can't be used for top-level entries, and look at the "inheritance for additions" idiom, which solves the data loss problem. We then investigate the implication of evolving classes used as key fields (in an entry) and conclude by proposing alternative designs.

Chapter 4: Simple distributed data structures

This is the first of two chapters that look at the forces affecting the design of distributed data structures. We start off by looking at some simple examples of ordered and unordered data structures and how we can use them to help us build a system that records and reports on errors encountered by other Jini services. Topics covered in this chapter include fixed/variable-sized arrays, stacks, queues, and a discussion about generic versus explicit arrays. The chapter concludes by looking at how to iterate over distributed data structures by using the "timeout iterator" idiom.

Chapter 5: Hierarchical distributed data structures

In the second of the two chapters about distributed data structures we look at how to design hierarchical data structures such as graphs and trees. We take the example of a large organizational hierarchy which is represented in XML and iteratively examine the forces that affect how we can go about designing a useful data structure to hold this data in a JavaSpace. Topics covered include how to edit subtrees, move and delete nodes, matching on subtree and tree walking.

Part 2: Mechanics

The second part of this book deals with some of the mechanisms behind space-based designs; these include behavior-based idioms, remote events, and code mobility, plus how to dynamically update your entry's behavior at runtime. This section presents an interesting set of idioms that build on some of the earlier examples by making space-based systems more dynamic and flexible.

Chapter 6: Codebases

Chapters 7 and 8 require an understanding of how to take advantage of the Java platform's ability to dynamically load classes at runtime from remote clients/services. This is a reference chapter which you can refer back to if you encounter any problems later on.

Chapter 7: Remote event-based idioms

In this chapter we take a look at how to use remote events effectively and some of the potential side-effects/pitfalls. We cover how to reuse event listeners and illustrate how the remote event's "handback" object can be used to dynamically select the correct handler for that event type. The chapter concludes by covering leasing, transactions, sequence numbers, and how the Event Mail Box service can be used in conjunction with a space-based client.

Chapter 8: Code mobility – behaviour-based idioms

This chapter starts off by looking at how code mobility plays a big part in he Jini world and how services register with the Jini Lookup Service. Using this knowledge we start to build a simple employee system that is dynamically updateable. Initially we implement a solution that uses subclassing, then we briefly delve into how class loaders work and present another simple model that allows you to update the behavior associated with an entry while the system is running. The chapter concludes by taking a brief look at the security issues that need to be considered when untrusted code is downloaded/executed.

Chapter 9: Using transient and persistent JavaSpaces

In this chapter we investigate how to use both transient and persistent JavaSpaces together in a single system. By using a marker interface with entries, we develop a model where short-lived entries can take advantage of a faster transient space, while longer-lived entries are written to a persistent space. In this chapter we introduce the concept of adapter proxies, which are also used in later chapters.

Part 3: Application

In the final part of the book we look at some practical applications for JavaSpaces. The first chapter in this section examines the well-known request-response model and describes how using JavaSpaces can result in more robust and flexible designs. The remaining chapters describe possible designs for bidding and location-based systems, followed by a look at how to build a logging system to track what's going in and out of a JavaSpace. We finish off by looking at some tuning tips.

Chapter 10: Process coordination

In the opening chapter of the final section, we take a look at the well-known request–response model used in many three-tier architectures, and examine how using JavaSpaces to decouple the participants results in more robust and flexible systems.

Chapter 11: Location-based services

This chapter looks at location-based services implemented with Spaces, and discusses how the use of generic or more abstract classes can help to reduce complexity and increase flexibility and future growth in these and other Spaces-based systems. We show how the inherent matching properties of Spaces can be used to implement powerful searching strategies, and make use of their inherent parallel character to optimize the searches.

Chapter 12: Bidding and agent systems

In this chapter we look at a very simple system in which agents bid to deliver parcels of various sizes in order to effectively share work and distribute resources.

Chapter 13: Testing space-based systems

Debugging space-based systems can be tricky, so in this chapter we present a simple, but flexible, logging model to record what's gone in and out of the space, who put it there or removed it. We examine several different models and logging strategies that you can use to help you figure out what's going on in the space.

Chapter 14: Tuning space-based systems

In this chapter we look at some of the details of building efficient and effective clients of a space; that is, those that use the resources available to them (in terms of the systems hardware) effectively. They not only go faster, but they also use memory and other computer resources well.

Appendix A: Code listings

This appendix contains the code listings to the utility classes used throughout this book, plus some of the classes we don't list in full.

Appendix B: Tips and rules in this book

This appendix contains a summary of the tips and rules dotted through this book, alongside the text.

The book's website

The source code for the examples in this book and the referenced utility classes can be downloaded from the book's website (http://jsip.info).


Chapter 1. Introduction

In this chapter we are going to take a look at JavaSpaces in general, starting with some foundation principles and outlining the terms that we intend to use throughout the book.

JavaSpaces should be thought of as a "place" in which it is possible to store and share information in the form of objects. JavaSpaces exist outside of other applications or services because they are themselves services, but applications and services that use them import parts of the space to interact locally with the JavaSpaces interface. We will take a look at how this works later in this chapter.

We will also be taking a brief peek "under the hood" to see how spaces are built and the differences between transient and persistent spaces. We are not going to go into great detail on the internal architecture of spaces, but in our experience a knowledge of how spaces work internally can greatly help with an overall understanding of how clients of the space interact and, hence, how space-based distributed systems work as a whole.

The remainder of the chapter covers some rules and tips we feel are important to bear in mind when reading the other chapters.


Foundations and terms

A JavaSpace is a Jini service that allows clients to share objects. Objects flow around space-based systems, allowing clients to share and store information and also, via the use of dynamic class loading, share behavior.

RULE

JavaSpaces contain and distribute Java objects that implement the entry interface at the top level.

In a Java program, all objects are derived from the base class Object. In JavaSpaces, all objects must also implement the Entry interface. So every object that a space is ever aware of is an entry (at least at the uppermost level). Entries can be arbitrarily complex and hence may represent a very simple unique identity, or may contain a very detailed complex computerized model, of an aircraft for example.

For the remainder of the book, where there is room for doubt, we use the term entry (or entries) to describe objects that are designed to exist in the space, whereas the term "object" refers to a "vanilla" Java object. In the first section of this book, we are going to look in detail at designing entries because they are at the heart of space-based systems design.

RULE

JavaSpaces associate entries.

Entries are found in spaces by association or matching. Spaces don't keep system-wide unique references to entries and hence don't have to maintain unique object IDs at the system level.

RULE

JavaSpaces are distributed.

A JavaSpace uses "templates" to match entries within the Space by their field's values and type. In this way JavaSpaces are different to other space implementations because they have strong typing semantics, but are similar to other spaces in that they do not use unique IDs to reference objects, but do use the value of the objects to do the matching.

JavaSpaces export proxies to clients dynamically, via the Jini lookup service, so they are naturally distributed. They consist of a combination of a proxy (the part of the space that gets distributed to clients) and the space server (or space back-end), which is the part that stores, matches, and distributes entries (see Figure 1.1).

Figure 1.1. JavaSpaces overview

Because JavaSpaces are distributed, the Application Programming Interface (API) accounts for exceptions that might occur with the underlying connection technology, for example cables, switches, routers, network stacks, etc., therefore not making the assumption that everything is working within this infrastructure. This is a good thing, especially in light of Peter Deutesh's seven fallacies of the network, which are:

  • The network is reliable.

  • Latency is zero.

  • Bandwidth is infinite.

  • The network is secure.

  • Topology doesn't change.

  • There is one administrator.

  • Transport costs are zero.

Of course, these are assumptions that we make when we build local objects systems because in this case they are usually true, and therefore very useful to work with. However, we must address these issues when we start to use distributed systems.

The design of JavaSpaces embodies some of these concerns, and hence forces us as system designers to take these factors into consideration when we build distributed systems. You will see this when we come back to look at the JavaSpaces interface and the possible exceptions thrown from an implementation of the interface.


Distribution using spaces

In a system that uses JavaSpaces, the space acts as an intermediary between the client and the server. In fact, the notion of client and server from the point of view of the user's implementation breaks at this point. If anything, the space has become the server, and the two communicating entities have become the clients.

To avoid confusion we call these clients services. As we go through this book you will see that we use the term "clients" to refer to all users of a space, and we use the language of peer-to-peer service communication to refer to the way these clients use the space.

You will find that as you introduce spaces into your systems it becomes less useful to think about the client/server model and more useful to think about communicating or collaborating peer services.


The JavaSpaces interface

We already know that services communicate in the JavaSpaces world by sharing entries.

Clients use the JavaSpaces interface to "write" entries into the space. They can then "read" the entries from the spaces – leaving a copy of the entry in the space for other clients to see – or by taking an entry, remove the Entry from the space.

The JavaSpaces interface does have other methods, but we won't describe them all in detail here. We will generally introduce the method signatures by example, but we will start here with the write method.

Write

Here is the write method as it appears in the JavaSpaces interface:

Lease write(Entry entry, Transaction txn, long lease)
   throws TransactionException, RemoteException;

Clients use the write method to write an entry into the space. The entry can be written under an optional transaction. If the transaction is null, then the written entry becomes visible to all space clients immediately, whereas when a transaction is used entries only become visible outside that transaction when it commits. In some of the examples in this book we use a null transaction for simplicity, but not where the use of a transaction is critical. As a general rule you should always use a transaction when you take an entry that is going to be written back to the space. If you don't use a transaction under these circumstances and your client crashes between taking the entry and writing it back, the entry is lost forever.

TIP

Always use a transaction with a take/write sequence.

The final parameter specifies the lease requested from the space (measured in milliseconds). The key word here is requested, because this method embodies a negotiation between the space client and the space. The method returns a Lease, which encapsulates the amount of time granted to the entry to live within the space. Leases granted by the write method are determined as part of the negotiation between the client and the space, and are finally set by the space implementation.

RULE

The JavaSpaces implementation is free to return a lease that is shorter than the requested lease, but not longer.

The obvious implication is that if you want to be absolutely sure how long an entry lives in the space then you should check the lease's expiry value and not simply assume that the full lease period has been granted. For purposes of simplicity, we generally make this assumption in our example code in this book, unless we are specifically dealing with lease time issues.

TIP

Use the Lease Renewal Manager or Lease Renewal Service to renew leases if the requested lease time is not granted by the space.

This method can throw one of two exceptions: a TransactionException is thrown if an error occurs with the transaction subsystem, and a RemoteException is thrown if there is a problem with communication between the proxy and the JavaSpace, or an exception propagates out of the space. In this way the spaces interface makes the fact that we are working in a distributed environment explicit rather than hidden.

Read

The read method allows us to get hold of a copy of an entry from the space without removing the "original" entry.

Entry read(Entry tmpl, Transaction txn, long timeout)
   throws UnusableEntryException, TransactionException,
          InterruptedException, RemoteException;

In this case the entry is serving the purpose of a template (hence tmpl) that the space uses to perform matching. Matching is done at the entry field level by comparing the serialized form of each of the non-null fields in all templates (a byte array) with entries existing in the space of the template's type or subtypes. A null field in a template is a wildcard, and will match any value.

RULE

When a transaction is passed to the read method the JavaSpace can return a matching entry written within the scope of that transaction, or one that is already visible.

If a non-null transaction is passed to the read method the space will attempt to match an entry that is visible to that transaction. Always keep in mind that entries already written to the space under a null transaction are visible to all transactions. This means that read could return an entry that hasn't been written under the transaction passed to the method.

Using a transaction with read has a second meaning.

RULE

If a matching entry is returned from read invoked with a transaction parameter, no other transaction can take that entry until the transaction either commits or aborts.

The final parameter to the read method is a timeout value that indicates how long you are prepared to wait for a matching entry to become visible. Due to the operation-ordering semantics of a JavaSpace, it is possible to "appear" to miss a new entry being written to the space on a blocking read or take – we look at how to code around this later in the book.

The read method can throw two additional exceptions to those in the write method. An UnusableEntryException is thrown if any serialized field of the entry being read cannot be deserialized, and an InterruptedException is thrown if the thread executing the read method is interrupted.

Take

The take method has the same set of parameters and possible exceptions as the read method; however, its function (as the method name indicates) is to take a matching entry from the space.

Entry take(Entry tmpl, Transaction txn, long timeout)
  throws UnusableEntryException, TransactionException,
         InterruptedException, RemoteException;

The semantics of the take method are the same as those of the read method, except that if a matching entry is returned it has been removed from the space.

ReadIfExists and TakeIfExists

Both the read and take methods have respective variants: readIfExists() and takeIfExists(). If either of these methods is invoked with a null transaction, they are equivalent to their counterpart being invoked with a zero timeout. The timeout parameter only comes into effect when a transaction is used. Under these conditions both methods wait for the timeout value to see if a matching Entry becomes visible under that transaction.

Snapshot and Notify

The remaining two methods in the JavaSpaces interface are snapshot(), which we discuss in depth in Chapter 14, and notify() which is covered in Chapter 7.


The structure of spaces

In this section we are going to look at how spaces are structured internally, at the architectural level. As we have already said, spaces are distributed, in that they are made from any number of space proxies that are dynamically loaded by space clients and the space server which stores, matches, and distributes entries (see Figure 1.2).

Figure 1.2. JavaSpaces internal structure

Reading Figure 1.2 from left to right, the client code has a view of the JavaSpaces interface, which it uses to move entries in and out of the space, as we have just described. The implementation of the client side of the space is hidden behind the interface in the space proxy. Various implementations of JavaSpaces obviously implement both this proxy and the spaces server in different ways and, as you will see in Chapter 9, we can also make our own bespoke implementations of this proxy to add features to our space-based systems. Of course, any implementation of the JavaSpaces interface should comply with the JavaSpaces specification.

The proxy is also responsible for preparing entries to be transported across the network. All the JavaSpaces implementations we have seen so far use the built-in RMI (Remote Method Invocation) and serialization mechanisms to prepare objects for the network. This is where entries get "flattened" by serializing their fields before they are streamed across the network.

Next we have drawn the underlying TCP/IP network protocol that is responsible for sending packets of information across the network to and from the space server. Again, this does not have to be the protocol used, but at the time of writing all systems use TCP/IP, thanks to the internet.

In the space server implementation on the right of Figure 1.2, we have shown the connection manager; this component is responsible for managing all the client connections to the space. The spaces server should in theory support any number of concurrent connections and handle requests fairly for all of the connected clients.

The heart of the server is responsible for storing and matching entries. This is shown as the indexing and storage component in the system. This is where Entries are shared by the spaces server between all the connected clients through the processes of indexing and storage. We have only shown the key subsystems here, but you will also find event notification and leasing subsystems within the space server.

Transient and persistent spaces

The last component is the subsystem, which supports space state persistence. Spaces can either be transient, which means that if the JVM (Java Virtual Machine) that they are running in fails for any reason, then the entries that are contained within the server will be lost, along with other information.

Persistent spaces use non-volatile memory, such as local disk drives, for storage and will hence retain entries even if the JVM running the space fails.


Summary

In this chapter, we have introduced some fundamental terms and rules regarding spaces, and shown how JavaSpaces are distributed.

Traditional client/server distributed object systems use a different set of mechanisms and ideas to distribute information. The use of spaces differs, because spaces associate objects via the spaces server, and this changes the way distributed systems are constructed when using JavaSpaces.

The JavaSpaces interface defines the relationship between JavaSpaces client services and the JavaSpaces implementation. We have described the write, read, and take methods that appear in the interface and looked at how spaces are structured internally, as well as seeing that spaces can be either transient or persistent.

This, of course, is not the whole story but it is an overview of some of the most important aspects of working with spaces. You will find much more information in Chapter 2 on many of these elements.


Part I: Entries

    Chapter 2. Designing entries

    Chapter 3. Evolving entries

    Chapter 4. Simple distributed data structures

    Chapter 5. Hierarchical distributed data structures


Chapter 2. Designing entries

Entries are at the heart of using JavaSpaces because they are the basis for all objects that are shared between all JavaSpaces clients. As such, they are the common thread for all object sharing between space clients.

Entries are basically Java objects, but in addition must also implement the interface net.jini.core.entry.Entry. Objects that are instances of Entry have special rules which apply to their use in JavaSpaces, and which exist outside the realm of the standard Java object idioms. For example, the Jini Entry Specification (part of the "Jini™ Technology Core Platform Specification", published by Sun Microsystems) gives a special meaning to the use of the access modifier public within the context of an Entry, overloading its meaning in the context of standard Java class design.

In this chapter we will be looking at how this new context affects the design of entry classes and the classes that interact with them. Because we need to consider two modes of usage for a single class, later on in the chapter we use the term "duality of use" to show where we are trying to answer these various design forces within the same class. You will see this as we introduce more design aspects in the chapter.

Before we go into detail on this, let's look back for a moment to Linda, the key forerunner to JavaSpaces, because a few words on its design make it easier to understand some of the design decisions made when discussing the structure of entries in the JavaSpaces model.


Background

Some of the underlying concepts for entries come from the world of Linda (see Gelenter, 1985), which is an environment for manipulating Tuples in spaces (TupleSpaces). A Tuple space contains Tuples whereas JavaSpaces contain entries. There are, however, some significant differences that deeply impact the use of entries versus the use of Tuples.

First, all entries have strong typing: that is, all Entry objects exist within the Java type system. It is hard to imagine an entity other than the built-in types existing "without" the type tree. Tuples, on the other hand, are untyped, hence can only be matched on value. So entries must be matched on type as well as support the value-based matching semantics that are sup-ported in Linda. Of course, subtype matching is also possible, which yields a very powerful set of matching idioms which we will look at throughout this book.

Second, entries are objects with an associated class, hence they can define behavior as well as state, which makes possible another set of rich patterns such as generic workers (see Freeman et al., 1999). These will be discussed later.

Possibly the most controversial aspect of the JavaSpaces specification is the incorporation of the Jini Entry Specification, which requires all "matchable" fields to be public. Since this breaks encapsulation, it introduces some interesting design decisions, which we will discuss throughout this chapter and the rest of this book.


An initial Entry

First we are going to take a "top-level" view of an example Entry. The example we will use in this chapter is based on a simple currency conversion system that could form part of a larger FX (Foreign Exchange) system.

We will use this top-level view to discuss some of the main ideas behind the design and construction of an Entry, and later drill down to discuss some of the details and outcomes involved.

Let's start by defining a simple Entry to represent an exchange rate.

import net.jini.core.entry.Entry;

public class ExchangeRateEntry
  implements Entry{

   public String _ccy;
   public Double _rate;
   public String _baseCcy;

}

The ExchangeRateEntry has three fields which hold the currency name _ccy, its rate _rate and the base currency name _baseCcy– these are declared public.

Public fields

Immediately alarm bells should be ringing. Why have these been made public? Surely good practice is to make these private, and to define accessor methods to update their values. We discuss this and related idioms in our first book Java in Practice (Warren and Bishop, 1999) so we are not going to recon-sider here why this is good practice. However, we need to keep in mind that once a class implements Entry a new context applies and hence we need to "read" the class definition in a new way.

TIP

If an object implements Entry (directly or indirectly) you should read public access of fields to mean "matchable and storable in a space".

The main point to remember is that if a class is a type of Entry (implements the net.jini.core.entry.Entry interface) then public fields are intended to be stored and matched in the space; that is, they become key fields.

A negative side-effect of using the keyword public to mean matchable and storable is that it still also means "give any other Java object public access to this field while the Entry object is active within this JVM". This can produce a degree of tension in the design forces applying to the "duality of use" of entries.

What happens if we want to store a top-level field in the entry but we don't want to match on it? Well, the short answer is that we can't, it is all (store and match) or nothing (don't store and don't match). Private fields are not stored in the space – this is counter-intuitive if we consider serialization.

Depending on your frame of mind and strength of your design notions (especially with regard to encapsulation) both the lack of private fields and the "duality of use" of the keyword public can be hard to get used to. To try and unbundle some of these tensions, later on in this chapter we present a set of idioms that try to regain some encapsulation.

RULE

All entries must implicitly or explicitly have a public no-args constructor.

The second key point to notice in our top-level view is that we do not use built-in types. This is because the value "null" also has a special meaning when we use entries as templates.

Finally, on our top-level view of entries, here's a very simple rule.

Constructors

In the ExchangeRateEntry class we haven't defined a public no-args constructor, because a class with no constructors (and no super-class with defined constructors) implicitly has a public default constructor: that is, if you don't define any constructors in a class, the compiler will add a default one for you.

TIP

Define both default and "populating" constructors for all Entry classes.

Our tip here is to define at least two constructors – the default constructor, which leaves all the fields null, and a "populating" constructor, which sets the key fields.

In light of this, we will update the ExchangeRateEntry class as follows.

import net.jini.core.entry.Entry;

public class ExchangeRateEntry
implements Entry{

  public String _ccy;
  public Double _rate;
  public String _baseCcy;
//required public no-args ctor
public ExchangeRateEntry(){
}

//ctor used for templates
//we never match on _rate
public ExchangeRateEntry(String baseCcy,String ccy){
   this(baseCcy,ccy,null);
}

//used to create
public ExchangeRateEntry(String baseCcy,
               String ccy,Double rate){
  _ccy=ccy;
  _rate=rate;
  _baseCcy=baseCcy;

  }
}

If you take a second look at ExchangeRateEntry you will see that it has three fields. The first two are self-explanatory, but you may not be familiar with the use of a base currency.

Using a base currency allows FX systems to convert an amount between two currencies without having to have a mapping of each currency against all others. Frequently the base currency is either USD (US dollars) or EUR (euros). Using this technique simplifies conversions and reduces memory and persistent storage requirements. So if we wanted to convert 100 GBP (pounds sterling) to JPY (Japanese yen) we would need to read both entries from the space and divide the amount (100 GBP) by the base currency rate, which converts it into the base currency. Then we would multiply the base currency amount by the JPY base currency rate, resulting in the JPY value for 100 GBP. The convert() method below shows one way to do this.

public static double convert(ExchangeRateEntry payoutccy,
    ExchangeRateEntry sourceccy,double amt)
       throws FXException{

    if(!payoutccy._baseCcy.equals(sourceccy._baseCcy)){
        throw new FXException(
                  "incompatible base currencies");
    }

  double srcRate=sourceccy._rate.doubleValue();
  double payoutRate=payoutccy._rate.doubleValue();
  return (amt/srcRate)*payoutRate;

}

Now that we have a basic algorithm for performing currency conversions we come to another next design decision – since Entry fields are public, should we be accessing them directly in the convert() method?

Entry design

There are two schools of thought when it comes to Entry design.

  1. Try to make Entries act like "real" objects.

  2. Don't try to make Entries act like "real" objects – treat Entries as data structures.

To date, most of the JavaSpaces examples we have seen tend to use a combination of these two approaches, encapsulating some aspects of an Entry, but occasionally accessing fields directly.

The second school of thought states that since entries are ordered collections of fields and can never be fully encapsulated, they should not be treated as first-class objects. It continues by stating that other first-class objects should perform all operations on entries.

This argument is compelling since we have to use public fields in entries. However, regardless of the new context we discussed earlier, accessing fields directly feels very unnatural when designing object-oriented (OO) systems, so we tend towards the first school of thought – try to make entries act like "real" objects.

The simplest technique is to simply provide get and set methods on the Entry class itself and be disciplined in your programming by only using these methods. However, this technique can easily be abused when we hand over code to other developers who are used to accessing the public fields of an entry directly. There are also other cases such as writing application-specific APIs which encapsulate the underlying technology (in this case, JavaSpaces) where hiding entries makes sense.

Several idioms with many variations can be used to make programming with entries feel more natural and provide a degree of encapsulation – we say "a degree of encapsulation" because the bottom line is that, whichever technique you use, an entry can always be instantiated by any process (with access to the class file) since it has to have a public no-args constructor. Having said that, if a client programmer is working to an API based on interfaces and has no (apparent) notion that a JavaSpace is being used under the hood then there is no reason to expect that developer to circumnavigate the effort we put into encapsulating entries.

The first technique we are going to look at is known as Entry Wrapping.


Entry Wrapping

Entry Wrapping is a simple idiom which encapsulates an entry by providing a wrapper with get/set methods for the public fields (of the "wrapped" entry) and an accessor method to get the underlying entry.

This idiom can be used in two ways:

  1. Declare the Entry as a public or static inner class of the wrapper.

  2. Provide a wrapper for an existing top-level Entry.

The first technique is to declare the entry as a public or static inner class of the wrapper class. A public or static inner class is required, as all entries must have a public no-args constructor. In the case of the entry being declared as a public inner class, the wrapper class must also have a public no-args constructor, whereas a public static inner class is effectively at the top level, so it can be instantiated without its parent class. This means that the parent class (of a static inner class) doesn't require a public no-args constructor.

Wrapper example

The ExchangeRateWrapper class below shows this idiom in action with the ExchangeRateEntry declared as a public static inner class.

public class ExchangeRateWrapper{

private ExchangeRateEntry _impl;

public ExchangeRateWrapper(String ccy,
             String baseCcy,Double rate){
   _impl=new ExchangeRateEntry(ccy,baseCcy,rate);
}

public ExchangeRateWrapper(Entry entry){
   _impl=(ExchangeRateEntry)entry;
}

public void setCcy(String ccy){
   _impl._ccy=ccy;
}

public String getCcy(){
   return _impl._ccy;
}

//other getters & setters

public Entry getEntry(){
   return _impl;
}

//the Entry class
public static class ExchangeRateEntry
     implements Entry{

    public String _ccy;
    public Double _rate;
    public String _baseCcy;

    public ExchangeRateEntry(){
    }
    ExchangeRateEntry(String ccy,
          String baseCcy,Double rate){
       _ccy=ccy;
       _baseCcy=baseCcy;
       _rate=rate;
    }
  }
}

Stepping through the class declaration, you will see that we first declare a pri vate field _impl, which will hold a reference to the underlying Exchange RateEntry. Next there are two constructors, one which is used to create an instance of the underlying ExchangeRateEntry and the other which is used to construct an ExchangeRateWrapper from an Entry – this one will be used when we retrieve an instance of the Entry from the space.

After the constructors there is a get and set pair of methods for the currency name which delegate to the _impl instance. We have omitted the getters and setters for the base currency and rate, but they follow the same pattern.

The next method declared is getEntry() which returns the underlying _impl instance (the actual Entry). This method will be used to get hold of the Entry to pass it into the space as either a template or for a write().

Finally, the static inner class ExchangeRateEntry is declared with two constructors; the required public no-args constructor and a convenience constructor that is used by the wrapper class.

You can use a similar technique to wrap an existing top-level entry by changing the first constructor to instantiate an instance of that Entry and then providing the getter and setter methods.

The example below shows how a wrapper class can be used to hide the underlying entry.

Client code

JavaSpace space=SpaceLocator.getSpace();

//create a wrapper instance
ExchangeRateWrapper cw=new
   ExchangeRateWrapper("GBP","USD",new Double(1.45));
//write it to the space
space.write(cw.getEntry(),null,300000l);

//use same object as template & take from space
Entry ccy= space.take(cw.getEntry(),null,100);

if(ccy!=null){
  //create a new wrapper with the returned Entry
  ExchangeRateWrapper cw2=new ExchangeRateWrapper(ccy);

  //print out the currency name
  System.out.println("Ccy name="+cw2.getCcy());
}else{
   System.out.println("Currency not found");
}

Using a wrapper class makes it harder to write "sloppy" code which accesses public fields directly but, as we mentioned earlier, there is always a way to circumnavigate the encapsulation of entries. For example, an ExchangeRateEntry instance can easily be instantiated as follows

ExchangeRateWrapper.ExchangeRateEntry ce
   =new ExchangeRateWrapper.ExchangeRateEntry();

The next techniques we are going to look at add another level of encapsulation and abstraction by defining an interface for each entry type and providing a factory to make instances of the entry.


Interfaces for entries

In the last section we used a simple wrapper class to encapsulate an entry that we declared as a public static inner class of the wrapper. This technique works well as it is relatively easy to implement and only introduces one additional class (the wrapper). However, the downside of using simple wrappers is that client code becomes tied to a concrete implementation (the wrapper), rather than programming to an interface.

Programming to interfaces rather than implementations is one of the cornerstones of OO design, as it results in loosely coupled designs – so it's something we should consider in the context of entries.

In this space-based idiom each concrete entry implements an interface, which declares a group of get and set methods. For our ExchangeRateEntry we would first define the interface that extends the entry interface.

Interface example

public interface ExchangeRate extends Entry{

   public void setCcy(String ccy);
   public void setBaseCcy(String baseCcy);
   public void setRate(Double rate);

   public String getCcy();
   public String getBaseCcy();
   public Double getRate();
}

Having defined the interface for the exchange rate class we then define the ExchangeRateEntry to implement this interface (which in turn extends the Entry ).

public class ExchangeRateEntry
    implements ExchangeRate{

   public String _ccy;
   public Double _rate;
   public String _baseCcy; //base currency

   public ExchangeRateEntry(){
   }
   ExchangeRateEntry(String ccy,String baseCcy,Double rate){
      _ccy=ccy;
      _baseCcy=baseCcy;
      _rate=rate;
   }
   public void setCcy(String ccy){
      _ccy=ccy;
   }
   public String getCcy(){
        return _ccy;
   }

   //other getters & setters
}

The UML (Unified Modeling Language) class diagram in Figure 2.1 shows the inheritance relationship between Entry, ExchangeRate, and ExchangeRateEntry.

Figure 2.1. Exchange rate hierarchy

Exchange rate hierarchy

With these changes in place, we can start to write code in terms of the ExchangeRate interface. A simple example of this may be a utility method in an API, which retrieves an exchange rate object.

Utility methods

public static ExchangeRate getFx(String ccy)
    throws Exception/*keep example simple*/{

   JavaSpace space=SpaceLocator.getSpace();
   ExchangeRateEntry tmpl=
       new ExchangeRateEntry(ccy,null,null);

   return (ExchangeRate)space.read(tmpl,null,0l);
}

The client code would then look something like this:

ExchangeRate ccy=FxUtils.getFX("USD");

Defining an interface for the ExchangeRateEntry allows us to write client code in terms of the ExchangeRate interface rather than tying us to the actual Entry class. Writing code to interfaces in this way means that at a later date a different entry could be used to store the exchange rate data in the space, but the client of the getFX() method wouldn't have to change.

Another example is that of the convert() method shown earlier, which originally took two ExchangeRateEntry parameters. This utility method can now be updated to use ExchangeRate parameters instead.

public static double convert(ExchangeRate payoutccy,
        ExchangeRate sourceccy,double amt)
   throws FxException{

   if(!payoutccy.getBaseCcy().equals(
                sourceccy.getBaseCcy())){

      throw new FxException(
              "incompatible base currencies");
}

double srcRate=sourceccy.getRate().doubleValue();
double payoutRate=payoutccy.getRate().doubleValue();
   return (amt/srcRate)*payoutRate;

}

An alternative design would be to declare a convertTo() method in the ExchangeRate interface as follows:

public double convertTo(ExchangeRate payout,double amt)
   throws FxException;

The convertTo() method could then be used in conjunction with the getFx() utility method, as shown below, where we want to convert 500 GBP into USD:

ExchangeRate gbp=FxUtils.getFx("GBP");
ExchangeRate usd=FxUtils.getFx("USD");

double val=gbp.convertTo(usd,500);

The next technique we will look at builds on the gains made by introducing an interface for our exchange rate entry and by providing a factory to create instances of ExchangeRate objects.


Factories for entries

In the previous section, we saw how the introduction of interfaces for entries allows us to write client code in terms of those interfaces. We also saw in the last code example that the getFx() method provided a wrapper over the space-based operations. These convenience methods can be useful for simple programs; however, for more complex space-based interactions (for example, with transactions) they can add more complexity than they hide. For this reason we need a better way of abstracting the use of Entries while still maintaining the gains we have seen though using interfaces.

One way to improve the design and solve some of these issues is to use the "object factory" idiom. An object factory, as its name suggests, makes objects – in our case we need a factory to make ExchangeRate objects. There are several variations of the factory idiom, which are beyond the scope of this book but are covered in our first book Java In Practice in some depth.

For the factory we are going to fully encapsulate the creation of an ExchangeRate object so we will need several overloaded make() methods, as shown below:

ExchangeRate gbp=FxFactory.make("GBP");
ExchangeRate yen=FxFactory.make("JPY","USD");
ExchangeRate eur=FxFactory.make("EUR","USD",new Double(0.65));

The first two overloaded make() methods will allow us to create ExchangeRate templates and the third make() method lets us create ExchangeRate (ExchangeRateEntry) objects to be written to the space.

Factory example

The FxFactory class shown below simply creates an instance of the ExchangeRateEntry and returns it to the caller.

public class FxFactory{

   public static ExchangeRate make(String ccy){
       return make(ccy,null,null);
   }
   public static ExchangeRate make(String ccy,
                 String baseCcy){
       return make(ccy,baseCcy,null);
   }
   public static ExchangeRate make(String ccy,
                 String baseCcy,Double rate){
      return new ExchangeRateEntry(ccy,baseCcy,rate);
   }
}

The code snippet below shows how to use the factory and ExchangeRate instances to interact with the space. Since the ExchangeRate instance extends the Entry interface, instances can be passed directly into space-based methods.

JavaSpace space=SpaceLocator.getSpace();

ExchangeRate gbp=
    FxFactory.make("GBP","USD",new Double(1.45));
//write the new ccy to the space
space.write(gbp,null,LEASE_LEN);

//create a template
ExchangeRate tmpl=FxFactory.make("GBP");

//read from space
ExchangeRate ccy=
    (ExchangeRate)space.read(tmpl,null,0l);

Looking at the above code, you will probably have noticed that it looks very similar to the code that used the wrapper class ExchangeRateWrapper. The main advantage in using the factory idiom with interfaces is that the client code will remain intact if we ever decide to use a different entry (other than ExchangeRateEntry) to store the exchange rate data in the space.


Design alternative

There is one area of the design that may concern you – the ExchangeRate interface which extends the Entry interface. By extending the Entryinterface in ExchangeRate, all ExchangeRate clients are also dependent on Entry. This type of dependency can be removed by declaring ExchangeRateEntry as implementing Entry instead; however, this would mean that all Exchange Rate instances would have to be cast to Entry for reads/takes etc.

The UML class diagram in Figure 2.2 shows an alternative inheritance relationship, where ExchangeRateEntry implements both Entry and ExchangeRate.

Figure 2.2. Alternative exchange rate hierarchy

Alternative exchange rate hierarchy

This alternative approach can also be used when you have an existing interface that you want to create an Entry for.


Entries for existing frameworks

In the previous sections we saw how to design interfaces and factories to encapsulate and abstract out the use of entries during the design of a new system. However, there are occasions where you might want to reuse existing code which may reside in a reusable library or framework.

Tariff analysis system

Imagine that we have an existing system that analyzes and compares tariffs (e.g. cellphone or electricity) from different utility suppliers, and uses a framework (written with reuse in mind) the interactions of which are based around interfaces. The existing system is running on a single server and is pushed to the limit, so we have decided to use space-based parallel computing to distribute the workload over several worker machines. To achieve this, we need to define entries that can map to the interfaces used by the existing framework. Let's first look at these interfaces and then see how to create entries that map to them.

There are two interfaces used by the existing system: a tariff and a usage profile. The system then runs the usage profile against each tariff and produces a report listing the cheapest tariff for the profile.

public interface UtilityTariff{

   //type of tariff e.g Cell Phone
   public String getType();

   //Name of supplier
   public String getSupplier();

   //start hour of low rate
   public int lowRateStart();

   //end hour of low rate
   public int lowRateEnd();

   //the low rate charge
   public double getLowRate();

   //the high rate charge
   public double getHighRate();

   //the weekend rate
   public double getWeekendRate();
}

The UtilityTariff interface defines a set of methods to access the low/high rates and the start/end of the weekend rate.

The UsageProfile interface below defines two methods: one to return the tariff type, e.g. cellphone, and one that returns a two-dimensional array of doubles – this holds the profile information. The profile is a 7 x 24 array representing average usage for each hour of the day and for each day of the week. The profile is generated by parsing billing information over the past six months. The system runs the profile information against the weekend, upper and lower rate tariffs to produce a report.

public interface UsageProfile{

   //tariff type e.g. cell phone
   public String getType();

   //7x24 profile data
   public double [][] getProfile();
}

Let's take the UtilityTariff interface first and define an Entry which can be mapped to that interface. The first point to note is that as an Entry cannot have primitive types as key fields, we will need to use Java's wrapper classes.

public class UtilityTariffEntry
   implements UtilityTariff, Entry{

   public String _type;
   public String _supplier;
   public Integer _lowRateStart;
   public Integer _lowRateEnd;
   public Double _lowRate;
   public Double _highRate;
   public Double _weekendRate;

   public String getType(){
       return _type;
   }
   public String getSupplier(){
       return _supplier;
   }
   public int lowRateStart(){
       return _lowRateStart.intValue();
   }
   public int lowRateEnd(){
       return _lowRateEnd.intValue();
   }
   public double getLowRate(){
       return _lowRate.doubleValue();
   }
   public double getHighRate(){
       return _highRate.doubleValue();
   }
   public double getWeekendRate(){
       return _weekendRate.doubleValue();
   }
}

Looking at the UtilityTariffEntry class, you will see that it implements both Entry (so it can be used with the space) and UtilityTariff (so it can be used with the existing framework). For each accessor method defined in the UtilityTariff interface, we have added a key field, using the wrapper classes to represent primitive types. Each accessor method that returns a primitive type delegates the wrapper class to return its underlying primitive value.

Defining an Entry for the UsageProfile interface is straightforward, as entries can contain arrays of primitive types – this is because an array is an object and can therefore be assigned to null, which is a wildcard in entry terminology.

public class UsageProfileEntry
    implements UsageProfile, Entry{

    public String _type;
    public double [][] _profile;

    public String getType(){
       return _type;
    }
    public double [][] getProfile(){
        return _profile;
  }
}

As you can see, mapping new entries to existing interfaces is relatively straightforward – for each primitive return type defined in an interface you use the equivalent wrapper class as a key field.

Master and workers

Having defined the base entries that map to the existing interfaces, the final step is to add the space-specific fields, which will allow the master and workers to interact. We won't go into the full details of the master/worker relationship here, but to complete the example we would need to add a unique identifier to the UsageProfileEntry and define a ReportEntry, which is keyed on that identifier. This would then allow the worker processes to take a UsageProfileEntry, iterate a distributed array of UtilitiyTariffs and then write back the analysis results (as a ReportEntry) to the space on completion. The master process would then pick up the ReportEntry and be able to map it back to the original UsageProfile.


The payload idiom

At the start of this chapter we mentioned that only public (key) fields in an entry are both "storable" and "matchable", which means that we cannot store an object that is assigned to a public field in the space without it being a candidate for matching. The downside to this is that the space implementation has to index all key fields whether we intend to match on them or not. To reduce the burden on the back-end space and in the hope that one day private fields will be stored, but not used for matching, an idiom known as "payload" has emerged.

TIP

Use the payload idiom to reduce the indexing burden on the space.

In simple terms, the "payload" is any serializable object (it may be an aggregation or a collection of other objects) which contains a state that needs to be stored in the space, but not matched on – ideally the payload would be declared as private if this was supported by the JavaSpaces Specification.

A simple example of a payload is the _rate field in the ExchangeRateEntry class, since this is a field we never intend to match on, but we need to have stored in the space. This example doesn't significantly impact the indexing burden on the space, as only a single field is involved. However, consider the UtilityTariffEntry class used in the last section. Which fields in it do you think are candidates for matching, and which are only used for storage?

Depending on the usage pattern of the system using the UtilityTariffEntry class, the chances are that the only key fields that would be used for matching are the _type and possibly the _supplier fields. This means that the remaining five fields would still have to be indexed by the space even though we don't intend to match on them – this is where the payload idiom comes in to reduce the burden on the space's resources.

To utilize the payload idiom, simply take all the key fields you don't intend to match on and create a new class containing these fields to act as the payload. So, for UtilityTariffEntry, the payload would look like this:

public class TariffPayload
    implements Serializable{

    private int _lowRateStart;
    private int _lowRateEnd;
    private double _lowRate;
    private double _highRate;
    private double _weekendRate;

    //accessors & constructors
}

and the UtilityTariffEntry class changes to include TariffPayload as a key field.

public class UtilityTariffEntry
    implements UtilityTariff, Entry{

    public String _type;
    public String _supplier;
    public TariffPayload _payload;

    //example accessors
    public int getLowRate(){
    return _payload.getLowRate();
    }
}

Finally, it is worth pointing out that since we have moved most of the key fields from the original UtilityTariffEntry class into a payload we no longer need to use the primitive type's wrapper classes.

TIP

Consider carefully which fields are candidates for matching before using the payload idiom.

The downside to the payload idiom is that you may need to "unroll" the payload at a later date if you ever decide that matching is required on one of the payload fields. Unrolling the payload means changing the entry and payload classes, which would result in problems if the space wasn't flushed – we discuss this and other related issues in Chapter 3.


Summary

Objects that are instances of net.jini.core.entry.Entry have special rules, which give them a new context and present different design challenges. All entries must have a public no-args constructor, whether this is declared explicitly or provided implicitly by the compiler. All public (non-transient, non-static, non-final) fields are matchable and storable, meaning that the space has to index every key field regardless of whether matching is intended or not. The payload idiom can be used to help reduce the indexing burden on a JavaSpace.

Non-public, transient, static, and final fields are ignored. Primitive types are not allowed. Entries can never be fully encapsulated, but they can be abstracted out into more elegant designs.

Defining wrappers for entries provides a simple and effective way to make entries appear encapsulated.

The use of interfaces and factories provides another way to encapsulate and abstract out entries, reducing the risk of developers having to access key fields directly.

Providing an interface for an entry means we can write code in terms of that interface rather than the concrete Entry class. The use of factories abstracts the creation of entries that implement these interfaces.

Mapping new entries to existing interfaces is relatively straightforward – for each primitive return type defined in an interface, use the equivalent wrapper class as a key field.


Chapter 3. Evolving entries

Almost all distributed systems have to evolve over a period of time to cope with new requirements. Frequently, new requirements result in changes to the underlying data structures used by the system. In systems that use a database this could mean changes to tables resulting in fields being added and/or removed. JavaSpaces are not immune to these changes, as Entry classes may need to evolve over time to meet new requirements, with fields being added/removed or class hierarchies being changed.

The JavaSpaces and Jini Entry Specifications do not mention Entry evolution. This means that different services or different implementations of the same service may behave differently when they encounter different versions of the same Entry class.

In Chapter 2 we discussed how classes that implement Entry have their own "new" context, which means that they must be treated differently to standard classes. The fact that Entry extends Serializable adds more interest to the discussion, as well as some confusion, in the context of class evolution.

In this chapter we will try to unravel some of this confusion and look at how the lack of a defined set of semantics for Entry evolution affects the design/maintenance of Entry classes. We will also investigate some more esoteric topics, such as how the matching semantics used by JavaSpaces can cause problems when classes used as "matchable" fields evolve.


Maintaining compatibility

The lack of defined semantics for how a Jini service should behave when it encounters different versions of the same Entry class, and the fact that Entry extends Serializable appear to be contradictory, as there is a defined set of semantics for how Serializable classes can be evolved.

As Entry implements Serializable, one would expect that the standard Java mechanism for evolution of Serializable classes would apply. That is, you might expect to be able to hardwire the Entry class's serialVersionUID to allow backward compatibility with previous versions. Unfortunately, even though all Entry classes are implicitly Serializable, hardwiring the serial VersionUID isn't going to help us here. For example, some JavaSpace implementations such as Sun's Outrigger (which comes with the Jini Starter Kit) will throw an UnusableEntryException if the number and types of matchable fields in an Entry template do not exactly match those of the Entry retrieved from the space – this is regardless of whether they have the same serial VersionUID or not.

To understand why this is the case, we need a refresher on how entries are serialized.

The Jini Entry Specification says that typically Entries are not stored directly, but stored in a service-specific representation that contains a serialized version of each "matchable" key field.

This means that the top-level Entry isn't (generally) serialized and explains why hardwiring a serialVersionUID (in most cases) will have no effect. There are other side-effects to this method of serialization, which we will discuss later, but to start with, let's consider the issue of data loss. This can occur when a class is evolved and used with standard serialization. Let's see how this relates to Entry serialization.


Standard techniques and data loss

The standard technique in Java used to evolve (serializable) classes allows multiple versions of a class to be compatible via serialization when they have the same serialVersionUID. However, this technique is far from perfect, since it doesn't address the issue of data loss.

Consider the following scenario, in which client #1 has an ExchangeRate class with three String fields:

public class ExchangeRate{

   private String _ccy;
   private String _baseCcy;
   private Double _rate;

   static final long serialVersionUID=1;
}

and client #2 has a later version of the ExchangeRate class, which has an additional String field to hold the date for which the _rate is valid. By the way, if you are wondering why we have chosen to use a String and not a java.util.Date, this is because if we tried to match on the _date field from a client in a different time zone it would fail – there's more about date matching in Chapter 14.

public class ExchangeRate{

   private String _ccy;
   private String _baseCcy;
   private Double _rate;

   private String _date;

   static final long serialVersionUID=1;
}

Client #1 passes an instance of the revised ExchangeRate class to client #2 which, say, modifies the _rate field and passes it back to the first client. When the object returns to the original client any data in the _date field will have been lost! (See Figure 3.1)

Figure 3.1. Serialization data loss

So although the two clients can interact with different versions of the same class, the outcome is unlikely to be behavior you wanted. Java's mechanism for class compatibility works well when data is going in one direction only, as it allows different clients to continue to interact with servers/other clients using "evolved" class versions. However, when classes are evolved to contain extra state in the form of additional fields the default mechanism can fail.

Let's now consider the data loss problem in the context of JavaSpaces where the ExchangeRate class is an Entry and one client uses matching on the _date field. It's not too difficult to imagine a situation in which an older client (without the _date field) could take an object from the space, modify the _rate field and write it back. If this was allowed to happen, the client using matching on the _date field would never be able to do this with a non-null field in a template.

Fortunately, this scenario cannot occur in current implementations, as we can't evolve entries in the standard way. Although this may not have been an intentional side-effect of the way entries are serialized, it actually works well in the context of JavaSpaces because it avoids "accidental" data loss.

Before leaving the topic of data loss, it's worth pointing out that none of this becomes an issue (with standard serialization) if you can control the distribution of classes from a centralized point, and insist that all clients shut down and restart when an upgrade becomes available. Under these circumstances, hardwiring the serialVersionUID would be a bad idea, as client programs that haven't been restarted could introduce the data loss problem – an alternative approach is discussed in Chapter 8 on code mobility.

Making the assumption that you will always be able to control how and when clients get upgraded may be impossible in the case of embedded devices, for example, in which the application's class files are burned on to a chip and are expected to be active for many years.


Using inheritance to add fields

As we can't use the standard refactoring techniques with entries we need to find an alternative approach. The approach to class evolution we will look at now uses inheritance to add key fields to entries. Continuing with the Exchange Rate example above, we could introduce a subclass that has an additional _date field.

public class ExchangeRate
   implements net.jini.core.entry.Entry{

   public String _ccy;
   public String _baseCcy;
   public Double _rate;
}

public class ExchangeRateWithDate extends ExchangeRate{
   public String _date;
}

At first sight the object-oriented aficionados among you will probably be wondering why we are suggesting using inheritance in this way, but hang on a second before you send us that nasty e-mail you're thinking about!

First, in general we are not keen on using inheritance in this way either, as it looks like inheritance for reuse. However, sometimes we have to be pragmatic and use workarounds as well as our favored OO solutions.

What may not be immediately obvious from the above code is that using inheritance in this "ugly" way is a workaround for the data loss problem we described earlier. This technique will work with both JavaSpaces and standard serialization so long as you have configured your environment correctly. For more detail see Chapter 6, but for now let's take a look at how and why this technique works in practice.

Figure 3.2 and the list below steps though the interactions with the space.

Figure 3.2. Using inheritance to add fields

The steps involved

  1. Client #1 sets its codebase etc. to allow remote classloading.

  2. Client #1 writes an instance of ExchangeRateWithDate to the space.

  3. Client #2 then attempts a take operation based on an ExchangeRate template.

  4. The space then matches the template against the ExchangeRateWithDate instance and returns it to client #2.

  5. The space proxy on client #2 then attempts to instantiate the concrete Entry class – ExchangeRateWithDate.

  6. If client #2 doesn't have this class in its classpath the RMIClassloader will step in and attempt to download it from client #1's codebase.

  7. Client #2 modifies the ExchangeRate details and writes them back to the space with the subclass fields intact.

The reason data loss won't occur when client #2 writes the Entry back to the space is because the concrete type is still ExchangeRateWithDate, even though the client (#2) doesn't know how to interact with it.

There are two other key points to note that allow this technique to work.

  1. The matching semantics of a space allow you to match on a super-class template. If this wasn't the case, we couldn't evolve Entries in the way we have just described.

  2. Code mobility allows clients to dynamically access and load classes they haven't seen before. For this to work, clients with additional classes must have a web/class server running, and set the system property java.rmi.server.codebase to point at the correct URL. There are more details on how this works in Chapters 6 and 8.

By looking at how entries are serialized, and taking advantage of space-matching semantics, we have arrived at a technique that allows us to evolve entries by adding new fields to subclasses. As a side-effect, we have also derived a technique that can be used outside this domain, which will avoid the data loss problem common with standard serialization. Using inheritance to achieve this may not be pretty, but it is effective.


Removing fields from entries

So far we have concentrated on evolving top-level entries by adding fields to them, and haven't considered what happens if we need to remove a field. The bottom line is that in most JavaSpace implementations you can't remove a key field without first flushing the space of all instances of the Entry you want to remove the field from. Failure to flush the space is likely to result in an UnusableEntryException when you try to match with incompatible classes, just as it would when a field is added.

We mentioned at the start of this chapter that the JavaSpaces and Jini Entry Specifications do not mention how a service should behave under these circumstances. However, Sun's Outrigger implementation appears to have set a precedent by warning users that entries are incompatible, in the form of an UnusableEntryException.

An alternative approach to bear in mind is not to remove the key field at all, but instead make it redundant. The problem with this approach is that the redundant field is likely to still have data associated with it, which will continue to get passed around within the system, unless of course you explicitly null that field before entries are written back to the space.

Some vendors now provide a Jini administration interface with methods for flushing out entries, which could be useful for persistent spaces. Whether the space is transient or persistent, it will probably have to go off-line for this process to take place safely.


Evolving nested key fields

Next we're going to take a look at what happens when you need to evolve classes that are used as matchable key fields; that is, classes you have written and that are declared as public (non-transient, non-final, non-static) fields in an Entry class.

To see how the evolution of a class that acts as a key field affects matching we are going to introduce a Currency class that the ExchangeRate class can use (as a matchable key field).

public class Currency
   implements java.io.Serializable{

   private String _ccy;
   //accessors
}

Note that Currency has to be declared as serializable otherwise we will get a NotSerializableException or an UnusableEntryException when we try to write the revised ExchangeRate entry to the space:

public class ExchangeRate
   implements net.jini.core.entry.Entry{

   public Currency _ccy;
   public Currency _baseCcy;
   public Double _rate;

}

Assume that we have instances of the revised ExchangeRate class in the space and we now decide that Currency needs an additional field _region. What's going to happen when we try the match using the new ExchangeRate._ccy as one of the key fields? It would be logical to expect to get an Unusable EntryException, but you won't – the match will simply fail. This is due to the matching rules for key fields, in which the bytes of the serialized field (recall that each Entry field is serialized individually) are compared with those in the template. So if you add any non-transient, non-final, non-static fields to a class such as Currency, matching will fail.

This problem is not restricted to the classes we write. For example, if java.lang.string was modified in this way in a future Java Runtime Environment (JRE) then clients using that JRE could never match on String fields written under an earlier JRE version!

The net effect of the matching rules means that we would need to implement readObject() and writeObject() in the revised Currency class to ensure that its serialized form is backward-compatible. However, this leads us straight back to the data loss problem described earlier. If you're really stuck, then this is an option, but as you will see later, changing the way you design Entries can help you to avoid these problems in the first place.

So how about trying the inheritance trick again?

Yet again we hit a stumbling block due to the matching semantics, because the bytes of a serialized subclass will obviously be different from those of its super-class.

This leaves us in a position where we can't safely evolve classes that are used as key fields. As with the removing of key fields from top-level entries, the only sensible option is to flush the space and repopulate it.


An alternative to nested key fields

The relationship between the Currency and ExchangeRate classes above is a natural one but, as we have seen, evolving classes that act as key fields safely is not possible with current JavaSpace implementations, so once again we need to rethink our design of entries to better cope with change.

TIP

Consider moving fields from nested classes to top-level entries.

We have already seen that using inheritance with top-level entries can support evolution in the form of adding new key fields, so if we move the fields from the Currency class back up to the top level we can get some extra flexibility – this technique is the reverse of the payload idiom introduced in Chapter 2.

If we apply this tip to the ExchangeRate and Currency classes, we end up almost back where we started.

public class ExchangeRate
   implements net.jini.core.entry.Entry{

   public String _ccy;
   public String _baseCcy;
   public Double _rate;
   public String _ccyRegion;
}

Now that we have moved the fields from the Currency class up in to the ExchangeRate class, we can apply the "inheritance to add fields" technique if we later need to add additional fields.


Problems with entry evolution

Although we have a technique for adding key fields to entries (one that implicitly avoids data loss), we still have to be cautious in our programming otherwise we may experience some unexpected behavior.

If we re-examine the interaction described earlier in the "inheritance to add fields" example, you may spot a potential problem (which we intentionally glossed over): that of accidentally losing entries.

TIP

Always use a transaction in conjunction with take operations.

If client #2 in the earlier example managed a successful take() on the space, without using a transaction, and client #1 hadn't set up a valid codebase, or a network error occurred while trying to download classes, then even though the take() will appear to the client to have failed, it will have succeeded on the space, resulting in a lost entry.

Another area where using evolved classes can result in unexpected behavior is when you are trying to match with an "evolved" subclass and older clients continue to write() entries into the space. For example, if a client is still writing ExchangeRate entries into the space and another client is trying to match with an ExchangeRateWithDate template, the space will only return instances of that class or any subclasses, not instances of its super-class ExchangeRate. This means that older clients can still interact with newer ones without data loss, but newer clients cannot match with evolved subclasses or older (super-class) versions.

A common solution to this problem is to write a process that takes all the older versions from the space (in a transaction) and then updates them with newer versions. Using this approach, it is possible to keep the space up without losing any data.


Summary

Evolving classes is complex in any domain; working with JavaSpaces is no exception. Different Jini services and implementations of the same Jini service such as JavaSpaces may behave differently when they encounter different versions of the same entry class.

You can use "inheritance for additions" to add new fields to entries without experiencing data loss. If you want to remove a field from an entry class then you either have to flush the space or leave the field redundant to allow backward compatibility.

Using evolving classes as key fields is possible, but this comes at the expense of data loss and writing more code (readObject() and writeObject()) to maintain backward compatibility.

Designing entries with evolution in mind by keeping all key fields as core types, such as String, allows us to use "inheritance for addition" later on.


Chapter 4. Simple distributed data structures

Designing data structures to use with JavaSpaces requires a fundamental shift in thinking to realize the benefits and gains that spaces bring to distributed systems.

It is very easy, when you first start programming with spaces, to use the same design of data structures that you would use in a more traditional distributed system – this is understandable because these are all tried and tested techniques. The simplest early mistake that most developers new to spaces make is that of writing an array to the space as a single entry. This is a natural first attempt at using a space because bulk gets and sets from remote servers are frequently used to cut down on network traffic. However, if the data structure is very large and you only need to modify a small part of it, such as a single element in an array, then you would in fact be using more bandwidth than was strictly necessary. The bulk approach under these circumstances also restricts concurrent updates, thus forming a bottleneck when more than one process needs to update (different parts of) the data structure at the same time. Another approach used in more traditional distributed systems is to send update requests to a server, which handles the request and updates the data structure.

Both of these techniques require some form of locking strategy to ensure that you avoid race conditions where multiple processes attempt to update the data structure at the same time. You would also have to implement some form of leasing policy for the bulk get and set strategy to ensure that the client which is modifying the data structure doesn't "crash", resulting in the possible loss of the whole data structure.

These types of problem are not present in space-based programming, as the semantics of the space enforce the removal of leased entries from the space to be modified. However, there is another set of design challenges which needs to be solved. In this chapter, we will be examining some of these challenges and the factors affecting the use of some simple space-based data structures.


Simple data structures

The simplest distributed data structure in space-based programming is called a "bag", which is an unordered collection of objects. In fact, you can think of JavaSpace as a whole as being like a bag because it is an unordered collection of entries: that is, if you read or take an entry based on a null template you could get back any entry of any type. For example, if we write three different types of entries, into the space and then do three takes with a null template, there is no defined order in which the entries will be removed from the space – this is determined by the space implementation.

Because the space is implicitly unordered, you don't need to do anything fancy to create a bag – simply write some entries of a known type into the space and use a non-null template (of the same type) to retrieve them. To illustrate this, we are going to create a simple "error alert" system, which collects internal error messages from other services and sends out e-mail and SMS (Short Message Service) text alert messages to interested parties. An alternative approach would be for services to send remote events to a mailbox and have a listening process handle them, but we'll leave it to you to consider the pros and cons of that design.

So let's start by defining a ServiceError entry for services to use to report internal errors.

public class ServiceError
   implements net.jini.core.entry.Entry{

   public String _serviceName;
   public String _occurredAt;
   public String _errMsg;

   public ServiceError (){
   }
   public ServiceError (String src,String msg){
      _serviceName =src;
      _errMsg=msg;
      _occurredAt=new java.util.Date().toString();
   }
   //accessors
}

The ServiceError entry declares three key fields, which are more or less self-explanatory. However, it's worth pointing out again why we are using a String and not a Date or Calendar field to store the time at which the error occurred. This is to avoid problems with time zone differences, and because we want to store the service-local time at which the error occurred, not the relative time on the machine where the entries are retrieved. There is more information on using Date and Calendar key fields in Chapter 14.

In this system, when a service encounters an error it simply creates a ServiceError object and writes it to the space – when the first entry is written, we have effectively created a bag of ServiceError entries.

The main part of the system is the process that takes the error messages from the space and sends out the e-mail/SMS messages. To do this, the process simply creates an empty ServiceError entry and uses it as a template.

long waitTime=1000;
ServiceError tmpl=new ServiceError();

while(Thread.isInterrupted()==false){
   ServiceError serviceError=space.take(tmpl,null,waitTime);
   if(serviceError!=null){
      notifyParties( serviceError );
   }
}

In the code above, we first created an empty instance of ServiceError to be used as a template, which allows us to take an entry from the ServiceError bag. As a bag is an unordered collection, we cannot make any assumption about which entry the space will return; that is, when using a bag every entry in it is a viable candidate to be picked, much in the same way as an item is in a "lucky dip". Bags work well for systems that don't need to process information in a defined order. This suits the error alert system we are developing, because initially we don't care about the order in which interested parties get notified, only that they do eventually get sent the error message.

TIP

Use polling with read and take operations.

One point of interest that you've probably picked up on in this example is why have we have used a loop with a specified wait time, rather than simply using Long.MAX_VALUE? This is for two reasons: to allow the process to be stopped gracefully via an interrupt() call on the running thread, but more importantly it avoids the problem where newly written entries can be missed due to the operation ordering semantics of a JavaSpace – it is possible that a take with an infinite wait time could miss a write (and never return). This may not be a problem if more matching entries get written to the space, but could be a problem if there is only a single matching entry written under these conditions.

Later on we will look at the notifyParties() method and how to store the interested parties in the space, but before we can do that we need to take a look at ordered data structures.


Ordered data structures

We have just seen how using unordered data structures (bags) with a space is easy. We simply define an entry and use an empty instance of that class as a template to access the entries in the bag. But what if we need to access entries in a defined order, such as a queue?

Designing a queue data structure is not complex; however, the processes and interactions involved in managing a queue are more difficult. So before digging deeper into how to manage a queue, let's look at the basics: the entries we need to define an ordered data structure, and then go on to examine the issues involved in managing one.

The basic building block for an ordered data structure such as a queue, channel, or stack is a distributed array. A distributed array differs from the type of array used in most programming languages in two fundamental ways; each element is a distinct entity rather than the array itself; and distributed arrays are frequently of a variable size, so they are more like a java.util.ArrayList than a standard Java array.

In contrast, an array in the Java language is considered to be an object. If you pass a reference to an instance of an array to another object, that object can obtain the size of the array and access each element directly. However, to access an element in a distributed array you have to take or read the element you want from the space, which introduces some interesting design challenges, such as, how do you know how big the array is, and how do you iterate over the array safely?

There's quite a bit to take on board here, so let's start by looking at the entries involved in defining a fixed-size distributed array, and then look at a variable size array.

Fixed-size array

First, we need an entry to represent an element in the array.

import java.io.Serializable;
public class ArrayElement
  implements net.jini.core.entry.Entry{

    public String _name;
    public Integer _index;
    public Serializable _data;

    //required no-arg constructor
    public ArrayElement(){
    }
    //constructor for matching
    public ArrayElement(String name,Integer index){
       _name=name;
       _index=index;
    }
    //constructor for creation
    public ArrayElement(String name,Integer index,
                               Serializable data){
        this(name,index);
        _data=data;
    }
}

Here, we are defining a generic array element that can contain any Serializable object as its data – recall that all key fields in an entry must be Serializable, so by defining the _data field as Serializable we can get the compiler to help us out by checking this at compile time rather than running the risk of getting a NotSerializableException (or an Unusabl eEntryException), which could happen if we declared the field as type Object and then assigned a non-Serializable object to that field.

TIP

Declare generic (object) fields in entries as Serializable .

The _name field will be used to uniquely identify an instance of the generic array in the space, and the _index field to hold the position of the element in the distributed array.

String arrayId="testArray";

//See Utility classes section for TxnManager
Transaction txn=TxnManager.createTxn(ONE_MINUTE);

int size=5;
for(int i=0;i<size;i++){

   Integer index= new Integer(i);
   String data="DATA"+i;

   ArrayElement e=new ArrayElement(arrayId,index,data);

   space.write(e,txn,Lease.FOREVER);

}
txn.commit()

In the above example, we picked an arbitrary lease time of one minute for the transaction, which should be sufficient to write the array elements to the space. However, bear in mind that if the array was larger or if the network was running extremely slowly the transaction might expire (and therefore abort) before all the elements have been written to the space. The best way to avoid this is to use the LeaseRenewalManager to renew the lease until the transaction commits or aborts.

As you can see, we now have a means of creating the elements for a distributed array and writing them into the space, but there is a problem – how does a client of the array know the size of the array?

If the array is read-only – that is, you know that no other process is going to take an element to remove it – then you can create a loop that increments the index and reads the elements until a null is returned.

String arrayId="testArray";
final long WAIT_TIME=500;
int index=0;
ArrayElement tmpl=
    new ArrayElement(arrayId,new Integer(index));
ArrayElement element=
    (ArrayElement)space.read(tmpl,null,WAIT_TIME);

while(element!=null){

  //use the element
  System.out.println(element._data);
  //increment the index
  index++;
  //update the template
  tmpl._index=new Integer(index);
  //read the next element
  element=(ArrayElement)
     space.read(tmpl,null,WAIT_TIME);

}

The "read until null" technique is fine as a starting point, but makes too many assumptions, such as the array starts at zero and if an element isn't returned within a predefined wait time then the end of the array has been reached. To improve on this, we can define another entry to hold meta-data about the array – its identifier and its size.

public class ArrayMetaData
  implements net.jini.core.entry.Entry{

    public String _name;
    public Integer _size;

    //required no-args constructor
    public ArrayMetaData(){
    }

    //for matching
    public ArrayMetaData(String name){
       _name=name;
    }

    //constructor for creation
    public ArrayMetaData(String name,int size){
       _name=name;
       _size=new Integer(size);
    }
    public int getSize(){
        return _size.intValue();
    }
}

To create our first distributed array we need to write an instance of the ArrayMetaData entry to the space in the same transaction as we write the ArrayElement entries.

Transaction txn
     =TxnManager.createTxn(ONE_MINUTE);//helper method

int size=5;
String arrayId="a1";

ArrayMetaData array=new ArrayMetaData(arrayId,size);
space.write(array,txn,Lease.FOREVER);

for(int i=0;i<size;i++){
   ArrayElement e=new
       ArrayElement(arrayId,new Integer(i),"DATA"+i);

   space.write(e,txn,Lease.FOREVER);

}
txn.commit();

Now that we have a distributed array in the space we can write the client-side code to access it. The example below first retrieves the array meta-data and obtains the size of the array by calling getSize(). Having got the size of the array, the example proceeds by reading each ArrayElement from the space and printing out its _data value.

String arrayId="a1";

ArrayMetaData tmpl=new ArrayMetaData(arrayId);
ArrayMetaData array=
    (ArrayMetaData)space.read(tmpl,null,Long.MAX_VALUE);

int size=array.getSize();
for(int i=0;i<size;i++){
   ArrayElement atmpl=
     new ArrayElement(arrayId,new Integer(i));
   ArrayElement element=
     (ArrayElement)space.read(atmpl,null,Long.MAX_VALUE);

   System.out.println(element._data);
}

As you can see from the above example, using a fixed-size array is relatively easy. However, most space idioms rely on variable-sized arrays.

Variable-sized array

So far, we have been using a fixed-size distributed array, which is fine when the number of elements is known and can be written into the space at once. However, this often isn't the case with space-based programs, so we now need to consider how to change the design to allow variable-sized arrays. The basics for the data structure are almost in place; it is how we add/remove and manage the elements that will determine the usage pattern; that is, is it a queue (First In First Out) or stack (Last In First Out)? For example, if we consider how a queue works we can state that it is an array of a variable size. Elements are added to the end, and the first element is the only one that can be removed. As the underlying data structure for queues, stacks, channels, etc. are variable-sized arrays, we need to modify the ArrayMetaData entry to reflect these different usage patterns.

To allow the distributed array to shrink and grow, we have removed the _size field and replaced it with two new fields: _headIndex and _tail Index. These will hold the start and end indexes of the array. We have also added a few helper methods to get the size of the array, to increment the head and tail, and to query if the array is empty.

public class ArrayMetaData
  implements net.jini.core.entry.Entry{

    public String _name;
    public Integer _headIndex;
    public Integer _tailIndex;

    //required no-args constructor
    public ArrayMetaData(){

    }
    //for matching
    public ArrayMetaData(String name){
       _name=name;

    }
    //for creation
    public ArrayMetaData(String name,int head,int tail){
       _name=name;
       _headIndex=new Integer(head);
       _tailIndex=new Integer(tail);
    }
    public boolean isEmpty(){
        return _headIndex.equals(_tailIndex);
    }
    public int getSize(){
        return _tailIndex.intValue()-_headIndex.intValue();

    public Integer getHead(){
        return _headIndex;
    }
    public Integer getTail(){
        return _tailIndex;
    }
    public void incrementHead(){
        _headIndex=new Integer(_headIndex.intValue()+1);
    }
    public void incrementTail(){
        _tailIndex=new Integer(_tailIndex.intValue()+1);
    }
}

The revised ArrayMetaData Entry allows us to manipulate the head and tail indexes, giving us the basic components to create other ordered distributed data structures such as queues, channels, and stacks, so let's get stuck in!

Regardless of the usage pattern, we first need to create an instance of the revised ArrayMetaData entry with a head and tail set to zero, and write it to the space.

String name="test";

ArrayMetaData array=new ArrayMetaData(name,0,0);

space.write(array,txn,Lease.FOREVER);

Once the initial ArrayMetaData entry has been written to the space, we can start adding elements to it. The list below outlines the necessary steps.

Adding objects to an array

  1. Create a Transaction.

  2. Take the ArrayMetaData.

  3. Get the tail index.

  4. Write a new ArrayElement with the tail index.

  5. Increment the tail index on the ArrayMetaData.

  6. Write the metadata back to the space.

  7. Commit the Transaction.

Having established how to add elements to the array, let's see what this looks like in code.

String arrayId"=test";
final long WAIT_TIME=500;
//Create a Transaction
Transaction txn=TxnManager.createTxn(ONE_MINUTE);

ArrayMetaData tmpl=new ArrayMetaData(arrayId);

//Take the metaData
ArrayMetaData metaData=
    (ArrayMetaData)space.take(tmpl,txn,WAIT_TIME);

//Loop to avoid missing a written entry
while(metaData==null){
    metaData=(ArrayMetaData)
        space.take(tmpl,txn,WAIT_TIME);
}
//Get the tail
Integer tail =metaData.getTail();

//Increment the tail
metaData.incrementTail();

//Create the element
ArrayElement e=new ArrayElement(arrayId,tail,"new item");

//Write element
space.write(e,txn,Lease.FOREVER);

//Write metaData back
space.write(metaData,txn,Lease.FOREVER);

//Commit
txn.commit();

We can use the same basic steps for adding items to queues and stacks, etc.

Using an array as a queue

To use a distributed array as a queue we need two methods, one to add objects to the queue, and one to remove them. We have already seen how to add objects to a distributed array, so let's look at the steps required to remove the object at the start of the queue.

Removing objects from a queue

  1. Create a Transaction.

  2. Take the ArrayMetaData.

  3. Get the head index.

  4. Take the element at the head index.

  5. Increment the head index on the ArrayMetaData.

  6. Write the ArrayMetaData back to the space.

  7. Commit the Transaction.

Here's what the code looks like:

String arrayId"=test";
final long WAIT_TIME=500;
//Create a Transaction
Transaction txn=TxnManager.createTxn(ONE_MINUTE);

ArrayMetaData tmpl=new ArrayMetaData(arrayId);

//Take the metaData
ArrayMetaData metaData=
    (ArrayMetaData)space.take(tmpl,txn,WAIT_TIME);

//Loop to avoid missing a written entry
while(metaData==null){
   metaData=(ArrayMetaData)
          space.take(tmpl,txn,WAIT_TIME);
}
//Get the current head index
Integer head =metaData.getHead();

//Wait until head element is available
ArrayElement etmpl=new ArrayElement(queueId,headIndex);
ArrayElement e=
    (ArrayElement)space.take(etmpl,txn,WAIT_TIME);

while(e==null){
   e=(ArrayElement)space.take(etmpl,txn,WAIT_TIME);
}

//Increment the head index
metaData.incrementHead();

//Write the update metaData back to the space
space.write(metaData,txn,Lease.FOREVER);

//Commit
txn.commit();

Having seen how to manipulate a distributed array as a queue, let's take a quick look at how to use a distributed array as a stack, before reviewing the design thus far.

Using an array as a stack

A stack is the reverse of a queue, as the last element is the one that gets removed (popped). We won't list the code for this, but here are the basic steps involved.

Pop

  1. Create a Transaction.

  2. Take the ArrayMetaData.

  3. Get the tail index.

  4. Take the tail element.

  5. Decrement the tail index.

  6. Write the ArrayMetaData back to the space.

  7. Commit the Transaction.

Improving the design

We have seen how to use a distributed array as a building block for other ordered data structures such as queues and stacks by manipulating an entry that contains metadata about the start and end indexes. This is fine, but it could result in a problem. This is to do with the lock (take) contention, when one process wants to take something from the array and another process wants to add something to the array – the operations are mutually exclusive, as each process needs to take the ArrayMetaData entry before proceeding.

In the case of a stack, this isn't a problem as competing processes both need to operate on the tail index. However, with a queue items are added at the tail but removed from the head. This means we could improve the performance of the queue by using two entries to represent the head and tail of the underlying array. The basic technique of manipulating the underlying array as a queue stays the same, apart from some minor implementation changes. There is also the additional use of a type (HEAD or TAIL) field.

public class ArrayMetaData
    implements net.jini.core.entry.Entry{

    public String _name;
    public Integer _index;
    public Integer _type;
    public static final Integer HEAD=new Integer(0);
    public static final Integer TAIL=new Integer(1);

    //required no-args constructor
    public ArrayMetaData(){

    }
    //for matching
    public ArrayMetaData(String name,Integer type){
        _name=name;
        checkType(type);
        _type=type;
    }
    //for creation
    public ArrayMetaData(String name,Integer type,int index){
        this(name,type);
        _index=new Integer(index);
    }
    //increment
    public void incrementIndex(){
        _index=
          new Integer(_index.intValue()+1);
  }
  private void checkType(Integer type){
      if(type.equals(HEAD)==false
          && type.equals(TAIL)==false){

          throw new IllegalArgumentException("Invalid type");
    }
  }
}

Using the revised ArrayMetaData we can improve the throughput of data, since the processes adding items to the queue won't be waiting while the processes taking items from the queue are busy. To create a queue, we need to write two instances of ArrayMetaData, one for the head and the other for the tail, into the space.

ArrayMetaData head=
    new ArrayMetaData(id,ArrayMetaData.HEAD,0);

ArrayMetaData tail=
    new ArrayMetaData(id,ArrayMetaData.TAIL,0);

To add items to the underlying array, create a template as shown below, take the ArrayMetaData from the space and follow the steps for adding an object to an array, as outlined above.

ArrayMetaData tmp=
    new ArrayMetaData(id,ArrayMetaData.TAIL);

To remove items from the queue, create a template with ArrayMetaData.HEAD as a parameter, and follow the steps for removing an item from a queue that we described earlier.

Before leaving the topic of distributed arrays it's worth considering the pros and cons of using a generic ArrayElement entry as we have in the above examples.

Generic vs explicit array elements

In the above examples, we defined and used a generic array element the data of which was any Serializable object. This approach is fine when you are only interested in reading or taking elements from the array based on a known index. However, there are occasions when you might want to search for a particular element in an ordered data structure based on a key field other than its index. This isn't a problem if the value assigned to the _data field in ArrayElement is something like a String, as the matching semantics of the space will work. However, if you are trying to match on a more complex object with some fields not assigned, this won't work. There's much more about these issues in Chapter 2, but as a general rule, if you think you are going to need to match on fields other than an index in a distributed array, you should define your own explicit array element.

One instance we have come across where it has been necessary to define explicit array element entries is when distributed array indexes have been used to indicate a ranking. A simple example of this is a music chart where the index represents a chart position. A distributed array could be used to facilitate ordered iteration over the elements to produce the "Hot 100", but there is also a requirement to find the chart position for certain artists.

This type of matching is better suited to a bag; however, to produce a ranking, you would have to take all the entries from the bag, sort them, and put them back or abort the transaction.

To solve this problem it's worth defining a reusable ArrayElementIndex entry that explicit array entries can extend – this isn't the kind of design decision we would make in non-space-based programming, but it is applicable here.

public class ArrayElementIndex
    implements net.jini.core.entry.Entry{

    public String _name;
    public Integer _index;

    //methods & constructors etc
}

The application-specific elements, such as chart position, can then be defined:

public class ChartPosition extends ArrayElementIndex{

    public String _artist;
    public String _songTitle;
    public Integer _weeksInChart;
    //methods & constructors etc
}

This technique allows us to produce rankings based on the array index and also to do matching on the _artist field to find out the artist's chart position and number of weeks in the chart. Had we defined ChartPosition as just Serializable and assigned instances to the _data field in the original ArrayElement class, we wouldn't be able to perform matching on the _artist field alone. Alternatively, we could have iterated the whole array and done the matching "by hand".

With all designs there are trade-offs that have to be made, and this one is no exception. This technique is worthwhile for very large distributed arrays that are being used in the manner described, but the same problem can be solved by using bags or by "matching by hand".

Distributed array example

Earlier in this chapter we started to design an error alert system in which we used a bag of ServiceError entries. These entries are written into the space by other services that encounter errors. The main error alert process would then take ServiceError entries and notify interested parties in the form of an e-mail or SMS text message.

Now that we have designed a distributed array, we can use it to store information about parties interested in particular service errors. First, we need a class to hold this information.

public class AlertContact
   implements java.io.Serializable{
   private String _e-mail;
   private String _cellPhone;

   public AlertContact(String e-mail{
     _e-mail=e-mail;
   }
   public AlertContact(String e-mail,String cell{
     _e-mail=e-mail;
     _cellPhone=cell;
   }
   public String getE-mail(){
      return _e-mail;
   }
   public String getCellPhone(){
     return _cellPhone;
   }
}

The AlertContact class is declared as implementing Serializable and not Entry because we are not going to write it to the space, but assign it to the _data field in ArrayElement.

Our plan is to use a distributed array (per service) to hold a list of AlertContact objects. The name of the array will be the same as the service name used in ServiceError, allowing us to map between the ServiceError and associated array of AlertContacts or, put another way, each service that reports errors has an associated distributed array.

To illustrate this let's assume that we have a service called News Ticker which reports errors. We therefore need to create a distributed array keyed on that name. Figure 4.1 shows the interactions with the space.

Figure 4.1. News Ticker

To keep the example simple and avoid repeating too much code from earlier, we are going to use a wrapper class that we have written called ExpandableArray, which encapsulates the distributed array creation, and the adding of elements, and provides a java.util.Iterator. A full listing of ExpandableArray can be found in Appendix A.

ExpandableArray array=
  new ExpandableArray("News Ticker");

//create the array
array.create( space );

Having created the array we can then start adding AlertContacts to it.

AlertContact contact=
   new AlertContact("alert@javainpractice.net");

array.add( contact );

We now assume that the space is fully initialized with a number of distributed arrays, one per service, so we can look at the code used by the main alerter process.

long waitTime=1000;
ServiceError tmpl=new ServiceError();

while(Thread.isInterrupted()==false){
   ServiceError serviceError=space.take(tmpl,null,waitTime);
   if(serviceError!=null){

      String serviceName=serviceError.getName();
      //get the associated array of contacts
      ExpandableArray array=
          new ExpandableArray(serviceName);

      //initialise the array
      array.init( space );
      //iterate over all contacts
      Iterator iter=array.interator();
      while(iter.hasNext()){
          AlertContact ac=
            (AlertContact)iter.next();

          //send e-mail/SMS messages
          sendAlert(ac,serviceError);

      }
   }
}

Looking at the above code, you will see that by using a wrapper class for data structures like the distributed array the code becomes far more readable and less obfuscated by space-specific operations.

Stepping though the code, we first take a ServiceError from the space and then use its name field to look up the array that contains the AlertContacts for that service. We then use the ExpandableArray wrapper to create an iterator to iterate over the underlying entries in the space. Finally we use the information in the ServiceError and AlertContact Entries to send an alert. We have omitted the code that actually sends the e-mails, as it is beyond the scope of this book; however, if you are interested in how this is done, check out the JavaMail™ API available from Sun Microsystems (see sun.java.com).


Iterating

Problems can occur in concurrent programs while iterating over shared data structures such as arrays. For example, what would happen if an element were removed from the middle of an array while we're iterating over it?

There are three common strategies, with many variations, used in non-distributed concurrent programs to handle this problem.

  1. Pessimistic: Take a copy of the array (in a synchronized block) and then use the copy to iterate over. This is sometimes referred to as a snapshot iterator.

  2. Synchronized: Use the read/writer pattern to allow multiple reader threads while no writer thread is active.

  3. Optimistic: Use the iterator until it is invalidated or an exception is caught – in the Collections framework a ConcurrentModificationException is thrown when underlying data structures have been modified.

Let's now consider these strategies in the context of JavaSpaces, and see if any of them are applicable. Taking a copy of a distributed array is not a viable option because by the time you have read all the elements into memory you have already iterated over the array, so there's no gain.

The synchronized strategy is well suited to space-based programming and the read/writer pattern is well documented in other texts, but we give a brief description of this later in this chapter.

Using an iterator until an exception is thrown is also not directly applicable here. However, we can use a simple optimistic strategy by implementing an iterator with timeouts on reads – we call this a timeout iterator.

Timeout iterator idioms

A timeout iterator works on the principle that if an element in an array isn't returned from the space within a specified timeout then it has reached the end of the array, even though this may not be the case. The timeout doesn't come into effect until we have retrieved the head of the array. We use this strategy in the ExpandableArray class (shown in Appendix A) as follows.

//from ExpandableArray
public Iterator iterator(long timeout)
    throws RemoteException{

    final long WAIT_TIME=500;
    try{

       ArrayMetaData tmpl=
           new ArrayMetaData(_name,ArrayMetaData.HEAD);

      //get the start index of the array
      ArrayMetaData head=
          (ArrayMetaData)
             _space.read(tmpl,null,WAIT_TIME);

      while(head==null){
          head=(ArrayMetaData)
          _space.read(tmpl,null,WAIT_TIME);
      }

      Integer startIndex=head.getIndex();
      //create an iterator
      return new IteratorImpl(timeout,startIndex);

    }catch(Exception ex){
       throw new
        RemoteException("failed to create iterator",ex);
    }
}

First, we read the current head index of the array and create an instance of a java.util.Iterator implementation, shown below.

class IteratorImpl
  implements Iterator{

  private long _timeout;
  private Integer _index;
  private Object _next;

  public IteratorImpl(long timeout,
                         Integer index){

    _timeout=timeout;
    _index=index;
  }

  public boolean hasNext(){
     try{

        ArrayElement tmpl=
            new ArrayElement(_name,_index);

        _next=_space.read(tmpl,null,_timeout);

        //increment the index
        _index=new Integer(_index.intValue()+1);

        return _next!=null;

      }catch(Exception ex){
             return false;
      }
  }
  public Object next(){
     return ((ArrayElement) _next)._data;
  }
  public void remove() {
     throw new
      UnsupportedOperationException();
  }

}

The iterator implementation works by attempting to read and cache the element at the current index in the hasNext() method. If the element has been returned by the space in the specified timeout, hasNext() returns true, allowing the client code to proceed by calling next(), which returns the cached element. However, if an element isn't returned by the space (for the current index, within the specified timeout) hasNext() returns false, indicating to the client program that the iterator has completed, although this may not actually be the case.

The timeout iterator idiom is not appropriate for all space-based programs; however, it does offer a simple and effective way of iterating over certain types of ordered data structures for systems that don't require the guarantee that an iterator will return all the elements in the underlying data structure.

There are a few variations to this idiom. For instance, the way we have implemented this idiom has made it prone to returning no elements if the head element is removed from the underlying data structure between reading the ArrayMetaData.HEAD entry and attempting to read the element at that index. One variation is to modify the code to take the ArrayMetaData.HEAD entry, read the element at the head index, and then write the ArrayMetaData.HEAD back to the space. However, this approach will cause more problems for completing processes trying to read the ArrayMetaData.HEAD entry.

Another variation is to read both head and tail indexes and increment the index in hasNext() until an element is returned from the space. This variation is safer than our simple implementation shown above, and also avoids the problem the previous variation introduced.

Read/write locks

This pattern allows multiple processes to perform reads until a process needs to write (update something in the data structure). When a write request arrives it will block until all the current readers have finished, and also block any further read requests until the writer has finished its work.

You can use this pattern for "safe" iteration, as it will guarantee that all readers have access to a stable data structure. However, the trade-off is that in a system with many readers and few writers, the writer processes may have to wait a while before they get their turn. The same is true if there are many writers and few readers. This problem is particularly obvious when iterating over large data structures.

This pattern is discussed in depth in JavaSpaces: Principles, Patterns and Practice by Freeman et al. (1999).


Summary

In this chapter we have seen how bags and arrays differ and how they can be used in conjunction to build simple, but effective, systems.

Bags are useful for write/take designs where the order that entries are returned from the space doesn't matter. These designs often take the form of one or more processes depositing entries into a bag. These are then taken and used by other processes. Bags are not suitable for writer/reader designs, as the reader may only ever see one entry – what you get back from a bag is space-implementation specific.

We have seen how distributed arrays can form the basis for other data structures such as queues and stacks, and how we can use a generic array element to hold any Serializable object. We have also seen that certain designs require explicit array elements, so that matching (on fields other than the index) and iterating can both be achieved.

Finally, we looked at different ways of iterating over arrays and examined some of the trade-offs that each strategy makes. Timeout iterators are unsafe, but don't introduce any contention between completing processes, whereas the read/write lock pattern is safe, but introduces contention.

In Chapter 5 we will look at some more advanced distributed data structures based around hierarchies of objects, by designing a distributed tree structure.


Chapter 5. Hierarchical distributed data structures

    Introduction

    Organizational structures

    Complex matching

    Generic distributed trees

    Summary


Introduction

In Chapter 4, we looked at two simple space-based data structures and examined some of the factors affecting their design. We saw that, rather than using a single entry to represent an array, we could split it up into several entries so that a single element could be read without retrieving the whole array from the space, allowing concurrent reads and writes to take place without "locking" out the whole array.

In this chapter, we are going to examine more complex data structures like trees using the same technique of splitting up the data structures into their constituent entries.

We will be investigating how splitting a hierarchical data structure, such as a tree, into several entries will allow space clients to access or update subhierarchies without having to take or read the full data structure. This is particularly important for hierarchies containing hundreds or thousands of nodes. For example, an investment bank may have a hierarchy representing the whole global risk structure right down to the book or position level, resulting in a hierarchy that could have tens of thousands of nodes. If a hierarchy like that was stored in the space as a single entry, taking or reading it would be incredibly slow and use a vast amount of memory.


Organizational structures

To illustrate how a hierarchical data structure might be used with a space we are going to base an example around a system that generates organization charts for a corporate intranet. Information about the corporate hierarchy will be stored in the space and accessed by other processes (possibly servlets) to generate a graphical chart based on a particular point in the organization's hierarchy – for example, a web client may request an organization chart of the sales force.

We will only be concentrating here on the space-based aspect of this system, not on how the data is ultimately visualized. However, if you are interested in building a system like this we will cover most of the building blocks you will need to implement it. Also, source code and examples on the book's website have the full implementations of snippets shown in this chapter.

Hierarchical structures such as organization hierarchies are often represented in XML (eXtensible Markup Language) format so let's start by taking a look at how an organizational hierarchy might be represented in XML. Don't worry if you're not familiar with XML, as we won't spend much time on the subject.

Organizational hierarchy in XML

<acmecorp>
  <sales>
    <paris>
      <manager>H. Smith

        <assistant>R. Hunt

          <secretary>A. Jones</secretary>

        </assistant>

      </manager>

    </paris>

    <!--other regions-->

  </sales>

  <!--other departments-->
</acmecorp>

Looking at the above XML code, you can see that "A. Jones" is a secretary working for "R. Hunt", who is the assistant of "H. Smith", the sales manager for "acme corp." in "Paris".

In a non-space-based Java program, we could represent a simple XML document by a single class, such as the XMLNode class shown below.

public class XMLNode{

  private String _xmlTag;
  private String _xmlData;
  private java.util.List _xmlNodes;

}

A tree would be constructed by creating the root node and recursively adding the child nodes, as shown below. Since this is just an example, we are showing the code line by line; however, in a real program we would use an XML parser to hide this.

//the root node
XMLNode acme=new XMLNode("acme");

//create all the subnodes
XMLNode sales=new XMLNode("sales");
XMLNode paris=new XMLNode("paris");
XMLNode manager=new XMLNode("manager","H Smith");
XMLNode assistant=new XMLNode("assistant","R Hunt");
XMLNode secretary=new XMLNode("secretary","A Jones");
//add all the subnodes
acme.addNode(sales);
sales.addNode(paris);
paris.addNode(manager);
manager.addNode(assistant);
assistant.addNode( secretary );

With this type of design the root node acme can be considered as the tree because it has no parent node. Because this type of data structure is self-contained it means that we can't use it with a space, as we don't want to have to take/read the whole structure just to use a subtree. So our first step is to use the principle of splitting data structures into constituent entries in a manner that will allow the entries to be matched on and accessed independently of the tree as a whole.

Distributed trees

For a distributed tree to be useful, we need to be able to read/take nodes from the space based on a couple of key fields: the unique name of the tree (e.g. the name of an XML document), and an identifier for the node (e.g. an XML tag). Each node will also need to have a field to hold any associated data (e.g. the XML tag's value). This information gives us a good starting point for a simple node entry.

public class SimpleNode
  implements net.jini.core.entry.Entry{

  public String _treeName;
  public String _nodeName;
  public Serializable _data;
}

As with the array example in Chapter 4, we are using a Serializable field to hold the data associated with the node. This is so the compiler can warn us if we try to assign a non-serializable object to the field.

Now that we have a starting point for a node entry we need to consider how we can associate subnodes (children) with it. A simple solution would appear to be to add an additional String field to hold the parent node's name so that we could create a template to get all the children associated with a parent node from the space. However, this presents two problems, one of which we encountered when we looked at bags in Chapter 4 – you have to take all the entries that match the template, as bags are unordered. The second problem is that parent names may not be unique – consider the tags in the example XML document, where the <manager> tag will appear in every region.

We solved the bag problem by switching to a distributed array in which each element has a consecutive index, allowing us to iterate over all the elements. We can apply a similar technique here by giving each node a unique identifier.

public class SimpleNode
   implements net.jini.core.entry.Entry{

   public String _treeName;
   public String _nodeName;
   public Serializable _data;
   public Integer _id;
}

The next thing to consider is how to associate a child node with its parent node. Again we could consider adding a 'pointer' back to the parent in the form of the parent's _id , but this wouldn't solve the problem either, as we wouldn't know the starting ID of the first child, and also the IDs won't be consecutive. A simpler approach is to use a design similar to that of the XMLNode class we looked at earlier but, instead of having a list of node objects as children, we have a list of child IDs.

Figure 5.1 shows this relationship, where the numbered boxes indicate the _id field and the arrows show how each node has a "pointer" to its children's IDs.

Figure 5.1. Simple node hierarchy

public class SimpleNode
   implements net.jini.core.entry.Entry{

   public String _treeName;
   public String _nodeName;
   public Serializable _data;
   public Integer _id;
   public List _childIds;
}

By associating the child IDs with the parent node, we will be able to read/take the parent node independently of its children, but this will also allow us to access the child nodes via their IDs. We'll have a look at the code to do that shortly, but first we had better complete the SimpleNode entry by adding the usual set of constructors and a helper method to add children.

import java.io.Serializable;
import java.util.List;
import java.util.ArrayList;

public class SimpleNode
   implements net.jini.core.entry.Entry{

   public String _treeName;
   public String _nodeName;
   public Serializable _data;
   public Integer _id;
   public List _childIds;

   //required no-args ctor
   public SimpleNode(){
   }
   //to create
   public SimpleNode(String treeName,
       String nodeName,Serializable data,Long id){

       _treeName=treeName;
       _nodeName=nodeName;
       _data=data;
       _id=id;
   }
   //other constructors for matching omitted
   //add a child id
   public void addChild(SimpleNode child){
        if(_childIds==null){
            _childIds=new ArrayList();
        }
        _childIds.add(child._id);
   }
}

Before we can create our first tree, we need to consider how to generate the IDs for the nodes.

Unique IDs

To make sure that we never duplicate a node ID, we need to generate a unique ID. The reason we can't simply start at zero and increment a static ID count each time a node is created is because clients other than the active one that created the tree will need to add nodes, but will have no simple way of knowing what the last-used ID was.

A heavyweight solution to this problem is to have a shared variable in the space that contains the tree name and the next available ID. However, this approach creates a bottleneck and slows down node creation. A better solution is to use an algorithm to generate globally unique IDs. One way to do this is to use one of the lesser-used objects in the java.rmi.server package – java. rmi.server.UID() – to help us create a globally unique ID.

The UID class generates IDs that are unique to the host that they are created on, based on the assumption that a reboot takes longer than one second. However, we need a globally unique ID. To achieve this we can take the hash code of the UID and XOR (^) it with the hash code from the local host's InetAddress.

Integer getId(){

   int hash1 = java.net.InetAddress.getLocalHost().hashCode();
   int hash2=new java.rmi.server.ObjID().hashCode();

   return new Integer(hash1 ^ hash2);
}

Generating unique IDs in this way is fine for this example and will work with many systems. However, to be super-robust, it is worth taking a look at how Jini creates ServiceIDs and then replace the Integer ID fields with an object similar to Jini's UUID.

Finally, we can get on to the fun stuff, and write a tree into the space.

Creating a tree

To keep things simple, we are only going to create a subsection of the XML document and write it to the space. You will notice that the code looks very similar to the non-space-based version we looked at earlier, with the exception that each node needs to be written to the space after its ID has been added to the parent node.

It's also worth pointing out why the method addChild() in SimpleNode takes a SimpleNode as a parameter rather than an Integer. This is for type-safety and to help avoid the wrong ID being added to the _childIds list.

String doc="acme.xml";

//build the hierarchy
SimpleNode manager=
    new SimpleNode(doc,"manager","H Smith",getId());
SimpleNode secretary=
    new SimpleNode(doc,"secretary","A Jones",getId());
SimpleNode assistant=
    new SimpleNode(doc,"assistant","R Hunt",getId());

//add the child nodes (ids)
manager.addChild(assistant);
assistant.addChild(secretary);

//See Utility classes for TxnManager & SpaceLocator
Transaction txn=TxnManager.createTxn(60000);
JavaSpace space=SpaceLocator.getSpace();

//write entries to space
space.write(manager,txn,Lease.FOREVER);
space.write(assistant,txn,Lease.FOREVER);
space.write(secretary,txn,Lease.FOREVER);

txn.commit();

Now that we have a simple XML tree in the space, we can look at the code to read a subsection of the document.

Reading subtrees

If we assume that the full XML document has been converted into a tree and is residing in the space, we can return to one of our original requirements – the need to be able to create an organizational chart from any point in the hierarchy. To be able to do this, we need the name of the hierarchy and the starting point (e.g. "sales") to create a template. In the example below we simply output the hierarchy as a tree.

First, we create a "Sales" node template and attempt to read the entry from the space.

JavaSpace space=SpaceLocator.getSpace();
String doc="acme.xml";

SimpleNode tmpl=new SimpleNode(doc,"sales");

SimpleNode sales=(SimpleNode)space.read(tmpl,null,1000);
if(sales!=null){
   //method to display the node
   displayNode(space,sales,"-");
}

To be able to display the subtree that starts at sales we need to write a recursive method to iterate the tree structure. The method displayNode(), shown below, takes three parameters: a reference to the space, the node to display, and a String to use as an indent.

static void displayNode(JavaSpace space,
                   SimpleNode node,String indent)
   throws Exception/*keeps example simple*/{

   System.out.println(indent+" "+node._nodeName);

   if(node._childIds==null){
      return;
  }

  Iterator iter=node._childIds.iterator();

  while(iter.hasNext()){
      Integer childId=(Integer)iter.next();
      SimpleNode childTmpl=new
         SimpleNode(node._treeName,null,childId);

      SimpleNode child=
          (SimpleNode)space.read(childTmpl,null,1000);

    if(child!=null){
       //increase indent and display child
       displayNode(space,child,indent+"-");
       }else{
          //could throw an exception here to
          //indicate that a child is unavailable
       }
    }
}

The displayNode() method first prints the name of the node passed as a parameter. It then checks to see if the node has any children by testing the _childIds field for null. If the field is not null then the code requests an Iterator from the _childIds field, and attempts to read the children from the space. If the child matching the template is returned within the specified timeout, displayNode() is called for that child node with an additional character appended to the indent parameter.

For the "sales" tree, the output from displayNode() will be as follows:

- sales
-- paris
--- manager
---- assistant
----- secretary


Complex matching

In the previous section we saw how to read and display a subtree within a hierarchy. This will work well for trees the nodes of which have unique names. However, as we pointed out earlier, this is an assumption that we cannot make with XML documents. For example, if we created a template for "manager" we could get back the node for marketing managers in New York, sales managers in Paris, or information technology (IT) managers in London, etc.

We can use the existing SimpleNode to achieve more complex matches like "sales managers in Paris", but in its current form it would prove inefficient. That is, to get the "sales manager in Paris" node we would have to read the "Paris" node then iterate its children (which means reading them from the space) to locate the "sales" node and then iterate that node's children to finally get to "sales managers in Paris".

To make matching more efficient, we could add an additional String field to SimpleNode that contains its path. For example, the "sales managers in Paris" node would have a path of acme/sales/paris. We could then create a template that has the path as the only matchable field.

For hierarchies that never change, this is a very good option as the space does something it's good at – matching. It is also very efficient, as you get straight to the node you want to use.

The downside of this approach is that it makes renaming or moving nodes more complex and inefficient. That is, if any node with children is renamed or moved, all its children's paths must be updated recursively. Another downside to using a path field is that the solution is not generic; that is, not all hierarchies will have names associated with nodes.

A less efficient, but more reusable, solution than adding a path field is to have each node contain the ID of its parent – something we will need later on when we look at deleting subtrees. Figure 5.2 shows how each node now has a "pointer" back to its parent node – this is shown as a dashed line.

Figure 5.2. Revised node hierarchy

public class SimpleNode
   implements net.jini.core.entry.Entry{

   public String _treeName;
   public String _nodeName;
   public Serializable _data;
   public Integer _id;
   public List _childIds;
   public Integer _pid;

   //constructors etc
   public void addChild(SimpleNode child){
      if(_childIds==null){
          _childIds=new ArrayList();
      }
      //set the child's parent id
      child._pid=_id;
      _childIds.add(child._id);
   }
}

Using this approach, we can eliminate the need to iterate over each node's children to get to the next level, by creating templates that have the parent node ID set. To make this approach easier to use, we have written a utility method to find a node from a full path e.g. acme/sales/paris. The method can easily be modified to take additional information, such as wait time, and a flag to indicate whether the final node in the path should be read or taken from the space.

static SimpleNode nodeFromPath(JavaSpace space,String [] path)
    throws Exception/*keeps example simple*/{

    String doc="acme.xml";

    SimpleNode tmpl=new SimpleNode(doc,path[0]);
    SimpleNode result=
        (SimpleNode)space.read(tmpl,null,1000);

    for(int i=1;i<path.length && result!=null;i++){

        //get the next node in the path
        //by using the parent ID
        tmpl=new SimpleNode(doc,path[i]);
        tmpl._pid=result._iD;

        result=
            (SimpleNode)space.read(tmpl,null,1000);
    }

    return result;

}

Using the nodeFromPath() method we would locate "sales managers in Paris" as follows. Note that the full path from the root node is required to return the correct node.

JavaSpace space=SpaceLocator.getSpace();

String [] path={"acme","sales","paris","manager"};

SimpleNode node=nodeFromPath(space,path);

The last example illustrated how to perform more complex matching on a tree. Next we are going to look at how to delete subtrees from a hierarchy in the space.

Deleting subtrees

In the previous section we introduced a _pid field in SimpleNode to hold the parent's node ID. This was partially to illustrate a different matching technique, but also because we need to be able to locate the parent node when a delete is performed. This is because SimpleNode maintains a list of its children's IDs.

To delete a node from the tree, we need to perform the following steps.

  1. Create a transaction.

  2. Take the node we want to delete.

  3. Take the parent node (if its parent ID is not null).

  4. Remove the node's ID from the parent's list of child IDs.

  5. Write the parent back to the space.

  6. Iterate the child nodes and recursively take them from the space without removing them from their parent (as this has been taken in step 2).

  7. Commit the transaction, or abort if any of the nodes can't be taken.

An important point to note when deleting nodes from the tree is that a configurable timeout should used for each take. That way, if any takes return null we can throw an exception (which will propagate out of the recursive method) allowing us to abort the transaction. By using this technique we can avoid deadlocks that might occur when one client is trying to edit part of the hierarchy while another client is trying to delete a part of it.

The method deleteNode() below shows how this can be done. We have also added a method deleteChild() to SimpleNode (not shown), which simply takes the ID of the child and removes it from its _childIds list.

static void deleteNode(SimpleNode tmpl,
            Transaction txn,JavaSpace space,
            boolean removeFromParent)
    throws Exception/*keeps example simple*/{

    //take the node
    SimpleNode sn=
        (SimpleNode)space.take(tmpl,txn,1000);

    //if the node isn't returned throw an exception
    if(sn==null){
        //using generic exception to keep example simple
        throw new Exception("Can't take node
"+sn._nodeName);
    }
    //remove from parent if we're not the root node
    //but we are the node being deleted (not a child)
    if(removeFromParent && sn._pid!=null){
        SimpleNode pTmpl=new SimpleNode(doc,null,null);
        pTmpl._id=sn._pid;
        SimpleNode parent=
            (SimpleNode)space.take(pTmpl,txn,1000);

        if(parent==null){
            throw new Exception(
                    "Can't take parent of "+sn._nodeName);
        }
        //delete is a new method on SimpleNode
        parent.deleteChild(sn);
        //update the parent
        space.write(parent,txn,Lease.FOREVER);
      }
      //now iterate and remove kids
      if(sn._childIds==null){
          return; //no kids
      }
      Iterator iter=sn._childIds.iterator();

      while(iter.hasNext()){
          Integer childId=(Integer)iter.next();
          SimpleNode childTmpl=
               new SimpleNode(doc,null,childId);

      //don't need to delete from parent
      deleteNode(childTmpl,txn,space,false);
    }
}

The deleteNode() method takes four parameters: a template for the node we want to delete, the transaction that will span the recursive algorithm, a reference to the space, and finally a flag to indicate whether the node needs to be removed from its parent. This is only set to true for when we invoke the method, and it is set to false in all recursive invocations. We could have split this method into two and avoided the use of an additional flag, but that would have meant us having to do an additional read first, to get the parent ID of the node we want to delete.

The code starts off by attempting to take the SimpleNode based on the parameter tmpl, and throws an exception if the entry cannot be taken, this will allow us to abort the transaction. Next, the parent node is taken from the space if the node isn't the root node and the removeFromParent flag is true (root nodes have a null _pid field). If the parent is successfully taken from the space, deleteChild() is called on the parent SimpleNode to remove the node we are deleting from its list. The parent entry is then written back to the space.

Finally, an Iterator is obtained from the node's _childIds list and each element is used to create a template to call deleteNode() recursively.

There are two other common types of operation associated with trees, moving nodes and editing nodes. Editing the data associated with a node is straightforward, it's just a take followed by a write in a transaction, but what about moving nodes?

Moving subtrees

Sometimes hierarchies need to be changed or reordered. For example, an assistant and her secretary might be asked to go and work in London from Paris. The hierarchy could be updated in two ways; by using the deleteNode() method and then recreating the entries under a different parent node or, more easily, by manipulating nodes' IDs.

To move a node within the tree we need to perform the following steps, which start off the same as those used to delete a node.

  1. Create a transaction.

  2. Take the node we want to move.

  3. Take the parent node (if its parent ID is not null).

  4. Remove the node's ID from the parent's list of child IDs.

  5. Write the parent back to the space.

  6. Take the new parent node.

  7. Update the node's parent ID to the new parent's.

  8. Add the node to the new parent.

  9. Write the node back to the space.

  10. Write the new parent back to the space.

  11. Commit the transaction, or abort if any of the takes failed.

As the algorithm for moving nodes is very similar to that for deleting nodes, we won't list it here. Instead we'll leave it as an exercise for you to try out.

Reviewing the design

As it stands, the SimpleNode entry works well for our example, as the _data field is only used as a String to hold a person's name. In cases where the nodes in a tree contain a large amount of data, in any operation that doesn't require the data field to be accessed (for example, moving or deleting a node) the node's associated data will be serialized along with the entry.

This means that in systems where a lot of nodes (with a large amount of associated data) get deleted or moved, it is worth splitting the node into two separate entries, one for the node and one for its data. This is what we will be looking at in the next section.

Another approach that crops up time and time again is expanding out the fields in the class assigned to the _data field and making them part of the node entry. This would produce a less generic design, but allow matching on fields that were formerly in the class assigned to _data, as these would become key fields.


Generic distributed trees

In the previous sections we developed a distributed tree to meet specific requirements. Although SimpleNode can be used for trees that have names associated with nodes, it is not fully generic. In this section we will be looking at how to design a generic distributed tree that can be used across many different types of system. To achieve this we are going to write a wrapper class to hide some of the complexity and to provide protection against the tree being corrupted by explicit client code manipulating the data structure incorrectly. Before we can start writing the wrapper class, we need to design some of the building blocks that are going to make this possible.

One of the key requirements of a generic distributed tree is storing any type of application-specific data associated with its nodes – this will also allow application-specific matching to be performed on the nodes. The first idea that might spring to mind is to use a Serializable field (for the node data) as we did in SimpleNode; however, since key fields in an entry are matched by comparing the byte arrays of their serialized form, this approach means that clients wouldn't be able to match on a subset of the node data. For example, if we assigned an instance of the class below to a Serializable key field we wouldn't be able to match on the _xmlTag field independently of the _xmlData field.

public class XMLData
   implements Serializable{

   private String _xmlTag;
   private String _xmlData;
   //methods
}

To solve this problem, we need to come up with a design that separates the node's data from the node and create two distinct entries – this type of design also means that the node data isn't returned with the node from the space.

Generic nodes

Having determined that not all nodes need a name field and that a Serializable field for the node data won't allow clients to match on a subset of the data object's fields, we can strip these fields out of the SimpleNode entry, resulting in a generic node entry.

public class Node
  implements net.jini.core.entry.Entry{

  public String _treeName;
  public Integer _id;
  public Integer _pid;
  public List _kids=new ArrayList();

  //constructors and methods omitted
  //e.g addKid() & removeKid()
}

The relationship between nodes is the same as we used in SimpleNode where the _list field contains a list of the IDs of the node's children; that is, the Node entry forms the structure of the tree based on IDs. This allows us to navigate the tree, but without any data associated with each node.

Next we need to define an entry to represent the data associated with a node. As each data entry (associated with a node) will need to have a tree name, an ID, and a parent ID (to assist navigation), it makes sense to use generalization and define a super-class that both Node and the data entry (NodeData) can inherit from.

The UML diagram in Figure 5.3 shows this relationship.

Figure 5.3. Node class diagram

Node class diagrams

The NodeIdentity class now contains the common fields.

public class NodeIdentity
    implements net.jini.core.entry.Entry{
    public String _treeName;
    public Integer _id;
    public Integer _pid;

    //constructors & accessors
}

The Node class now looks like this:

public class Node extends NodeIdentity{

  public List _kids=new ArrayList();

  //constructors and methods omitted
  //e.g addKid() & removeKid()
}

Finally we define a NodeData class which will act as a marker entry and give us the type information we need for reading and taking entries from the space.

public class NodeData extends NodeIndentity
//marker Entry that will be subclassed
}

What may not be immediately apparent at this point is why NodeData extends NodeIndentity. The motivation for NodeData extending NodeIndentity and therefore having both _id and _pid fields is to allow us to perform matching directly on a NodeData entry – this comes into play when we have a matching NodeData entry that we want to delete or edit. The trade-off is that we can simplify editing/moving and deleting nodes at the expense of duplicate data, in the form of the _pid field being both the Node and NodeData entries.

If you take another look at the NodeData class you will see that we haven't provided a field to hold any data. This is because we are going to take advantage of the fact that a space allows us to do matching on super-class entries, meaning that application-specific data can be assigned to subclasses of NodeData.

Sticking with the earlier example, we can define an XMLNodeData entry that extends NodeData.

public class XMLNodeData extends NodeData{

    public String _xmlTag;
    public String _xmlData;
}

Figure 5.4 shows the revised UML diagram. The subclassing technique will allow us to design a wrapper class for the distributed tree which can manage the relationships between the nodes, their children and associated data. Clients of the wrapper class will then be able to safely add instances of their application-specific NodeData subclasses.

Figure 5.4. Revised node class diagram

For the remainder of this section, we will be stepping through the process of designing a wrapper class for the generic distributed tree.

Designing a wrapper class

Wrapper classes are common in space-based programs as they provide a convenient way to encapsulate interactions with the space, and can provide some form of guarantee that the integrity of the underlying data is maintained, rather than letting each developer figure out how the entries should be managed. Wrappers also make client programming much easier, as a well-written wrapper can (almost) hide the fact that a space is involved.

Wrappers need to be initialized with a reference to the JavaSpace and a TransactionManager. This is usually done via a constructor. For our DistributedTree, we are also going to have a String parameter that is used to specify the name of the tree. An alternative approach to passing in a TransactionManager would be to let the DistributedTree discover one itself; however, we have chosen the option of passing in a Transaction Manager as you might want to select a specific instance for performance or reliability reasons.

public class DistributedTree{

    private JavaSpace _space;
    private TransactionManager _txnMgr;
    private String _treeName;

    public static final Integer ROOT_ID=new Integer(0);

    public DistributedTree(String name,
           JavaSpace space,TransactionManager txnmgr){

      _space=space;
      _txnMgr=txnmgr;
      _treeName=name;
    }

}

We have also declared the constant ROOT_ID to make retrieving the root node easier. Next we are going to need a method to create a new tree.

Creating a tree

To create a tree, we need to write a Node to the space with its id field set to ROOT_ID, and also write the node's NodeData. The create() method below can be made more robust by adding a check to see if a tree with the same name already exists.

public Node create(NodeData data)
    throws Exception/*keeps example simple*/{

    Transaction txn=null;
    try{
       Transaction.Created txnC
        =TransactionFactory.create(_txnMgr,5000);
       txn=txnC.transaction;

       //create a root node with the constant
       //ROOT_ID
       Node root=new Node(_treeName,ROOT_ID);
       //set the id & tree name on the data node
       data.init(id,_treeName);

       //write both entries to space
       _space.write(root,txn,Lease.FOREVER);
       _space.write(data,txn,Lease.FOREVER);
       //commit the txn
       txn.commit();
       return root;

    }catch(Exception ex){
        //if anything goes wrong abort
        if(txn!=null){
            try{txn.abort();
            }catch(Exception){/*ignore*/}
        }
        //rethrow the exception
        //using jdk1.4 chained exceptions
        throw new Exception(
              "Unable to create tree",ex);
    }
}

To create a tree for an XML document you would first instantiate a DistributedTree object and then create an instance of the XMLNodeData to call create() with.

//utility classes in jsip.jini package
JavaSpace space=SpaceLocator.getSpace();
TransactionManager txnMgr=TxnManager.getTxnMgr();

DistributedTree tree=new DistributedTree("acme.xml", space,
                                         txnMgr);

XMLNodeData root=new XMLNodeData("acme",null);

Node rootNode=tree.create(root);

Now that we have created a root node for the tree, we need to be able to add nodes to the tree.

Adding nodes to the tree

To add nodes to the tree, we need a parent node ID and a NodeData (subclass) entry. This allows us to create a new Node and add its ID to the parent nodes list. We will also need to create a unique ID for the new node. To recap, here's an algorithm to generate unique IDs. Either this method or the calling method should also check that the new ID isn't the same as the ROOT_ID .

private Integer getId()
    throws UnknownHostException{

    int hash1= InetAddress.getLocalHost().hashCode();
    int hash2= new java.rmi.server.ObjID().hashCode();
    return new Integer(hash1 ^ hash2);
}

The steps for adding a node are as follows:

  1. Create a transaction.

  2. Take the parent node – throw an exception if it's not returned.

  3. Create a new Node.

  4. Set the tree name and new node ID on the NodeData.

  5. Add the new node to the parent node.

  6. Write the new node to the space.

  7. Write the NodeData to the space.

  8. Write the parent node back to the space.

  9. Commit the transaction, or abort if anything goes wrong, or the entries cannot be taken from the space within a specified timeout.

  10. Return the new node.

The algorithm for adding nodes is very similar to that shown in the previous sections, so we won't list it here, but for reference there are two method signatures. The first is for direct usage and the second is for subclasses to use – we'll take a look at the second one a little later on.

public Node addNode(Node node,NodeData data);

protected Node addNode(Integer pid,NodeData data)

Continuing the last example, we can use the rootNode to add a child node, and so on:

Node salesNode=
   tree.addNode(rootNode, new XMLNodeData("sales",null));

We also need to add a method to DistributedTree that allows clients (other than the one who created the tree) to get the root node:

public NodeData getRoot()
      throws Exception/*keeps example simple*/{

    Node rootTmpl=new Node(_treeName,ROOT_ID);
    return _space.read(rootTmpl,null,1000);
}

The full source code for the DistributedTree is available on the book's website.

Walking the tree

Earlier on, we looked at how to read and display a subtree of SimpleNode entries. Now that we have separated the data from the Node entries, the process becomes slightly more complex so it is worth providing a method to return a list of children from a NodeData entry. The getChildren() method below shows how this is done.

public List getChildren(NodeData nd,long waitFor)
    throws Exception/*keeps example simple*/{

    List children=new ArrayList();

    //first get the node for the NodeData
    Node tmpl=new Node(_treeName,nd._id);
    Node node=(Node)_space.read(tmpl,null,waitFor);
    if(node==null){
        throw new Exception("Can't read node");
  }

  if(node._kids!=null){
      Iterator iter=node._kids.iterator();
      while(iter.hasNext()){
          Integer childId=(Integer)iter.next();
          NodeData childTmpl=
              new NodeData(childId,_treeName);
          NodeData childNd=
              (NodeData)_space.read(childTmpl,null,waitFor);
          //if we can't read the entry throw an exception
          if(childNd==null){
              throw new Exception("Can't read child data");
          }
          children.add(childNd);
      }
  }
  return children;
}

Stepping through the code, you will see that we first get the Node entry with the same ID as the NodeData entry. This allows us to then iterate through the child IDs and read the associated NodeData entries.

The getChildren() method can then be used by clients to recursively "walk" the tree.

Deleting and moving nodes

To delete a node from the tree we first need to take the parent node and remove the ID of the node we want to delete (from the parent node), and then recursively take all the child nodes and their associated data.

The steps for deleting a node are the same as those we described in the SimpleNode example, but the associated NodeData entries must be taken from the space too.

Moving nodes within a hierarchy also follows the same steps we looked at in the previous section, but the NodeData entries must have their parent IDs updated, as well as the Node entries.

Finding nodes

As we have abstracted out the application-specific data into subclasses of NodeData, our wrapper class could only provide helper methods to abstract the usage of the space. For example, we could add a findNode() method which simply attempts to read the NodeData from the space based on a template parameter; however, this would not be worthwhile. A better solution is for each tree type (e.g. an XML document) to subclass, or delegate to, the DistributedTree and provide the type-specific methods needed to locate nodes in that tree. We saw an example of this in the previous section in the method nodeFromPath().

To assist subclasses, we need to add a few helper methods to DistributedTree.

//for subclass access
protected JavaSpace getSpace(){
     return _space;
}
protected String getTreeName(){
     return _treeName;
}

The important thing is for the DistributedTree to manage the underlying data structure for adding nodes, edits, deletes, etc.

Right at the start of this chapter we looked at organizational hierarchies and designed a SimpleNode entry to hold data based on a sample XML document. Now that we have created the building blocks for a generic distributed tree, we can revisit the design and see how it fits in.

To recap, the system we are designing is to allow users to navigate an organization's personnel hierarchy and produce an organization chart. There are two ways in which client code (possibly a servlet) will need to interact with the system to produce the chart; either by drilling down from the top level or by drilling down from a known point, e.g. "Sales in Paris". We can use the get Children() method shown earlier to fulfill this requirement, but before we can do this we need to define an entry to hold the data associated with each node. For simplicity, the OrgNodeData class only stores a name and a value.

public class OrgNodeData extends NodeData{
   public String _name; //e.g. manager
   public String _value;//e.g. R Hunt
   //constructors & accessors
}

We can now use the OrgNodeData class with the DistributedTree class and create a tree as shown earlier. However, as we intend to subclass the DistributedTree to provide methods to find nodes, it would make sense to make the subclass encapsulate the notion of Node objects and provide a set of methods that take and return OrgNodeData objects. Taking the time to do this will make both the client programmers' lives and ours much easier.

The code below shows an example of this in practice, where OrgTree is a subclass of DistributedTree, which provides methods in a client-friendly way.

public class OrgTree extends DistributedTree{

   public OrgTree(String name,
       JavaSpace space,TransactionManager txnMgr){
       super(name,space,txnMgr);
   }
   public OrgNodeData getRootNode()
       throws Exception/*keeps example simple*/{

       JavaSpace space=getSpace();

       NodeData tmpl=new NodeData(ROOT_ID,getTreeName());
       return (OrgNodeData)space.read(tmpl,null,1000);

   }
   public Node addNode(OrgNodeData parent,OrgNodeData child)
       throws Exception/*keeps example simple*/{

       //using the protected super-class addNode
       return /*super.*/addNode(parent._id,child);

   }
   public OrgNodeData [] getChildren(OrgNodeData nd,
                                               long waitFor)
       throws Exception/*keeps example simple*/{

       List children=super.getChildren(nd,waitFor);
       OrgNodeData [] childArray=new
                               OrgNodeData[children.size()];
       children.toArray(childArray);
       return childArray;
   }
}

Now that we have the basics in place, we can implement a method to find a particular point in the organization's hierarchy.

public OrgNodeData getByPath(String [] path,long waitFor)
    throws Exception/*keeps example simple*/{

    JavaSpace space=getSpace();
    String treeName=getTreeName();

    OrgNodeData tmpl=new OrgNodeData();
    tmpl._treeName=treeName;
    tmpl._name=path[0];
    OrgNodeData result
        =(OrgNodeData)space.read(tmpl,null,waitFor);

    for(int i=1;i<path.length && result!=null;i++){

        tmpl._name=path[i];
        tmpl._pid=result._id;
        result
           =(OrgNodeData)space.read(tmpl,null,waitFor);

    }
    return result;
}

Finally, here's an example of how to display part of the tree to the console by using the getChildren() method above. As with the earlier example, the indent parameter is used to make displaying a tree on the console easier to visualize.

void display(OrgNodeData nd, String indent)
     throws Exception{

     System.out.println(indent+" "+nd);
     OrgNodeData [] children=getChildren(nd,500);
     for(int i=0;i<children.length;i++){
     //recursively call display
     display(children[i],indent+"-");
  }
}

Updating nodes

So far we have looked at adding, deleting, and moving nodes. The algorithms we are using are designed to "drop out" and abort the transaction if a node cannot be accessed within a parameterized timeout period. By using timeouts and transactions, we can avoid deadlocks that may occur when another process is trying to update a subsection of the data structure. Using a transaction around an update maintains the reliability.

To update a NodeData subclass we need to do the following.

  1. Create a transaction.

  2. Take the NodeData entry.

  3. Modify the NodeData entry.

  4. Write the NodeData entry back to the space.

  5. Commit the transaction, or abort if anything goes wrong, or the entries cannot be taken from the space within a specified timeout.

Assuming that the IDs aren't being altered, we don't need to take the associated Node entry when updating a NodeData entry. This is because both move and delete operations abort if any Node or NodeData entry can't be taken from the space (that is, the taken NodeData in this instance acts as the lock).

As we are trying to encapsulate the complexity of the underlying tree structure in DistributedTree, it would make sense to encapsulate the edit process. To achieve this we are going to introduce a new class called EditableNodeData, which has three methods: getData(), commit(), and abort().

public class EditableNodeData{

    JavaSpace _space;
    Transaction _txn;
    NodeData _nd;

    EditableNodeData(JavaSpace space,
        Transaction txn,NodeData nd){

        _space=space;
        _txn=txn;
        _nd=nd;
    }
    public NodeData getData(){
        return _nd;
    }
    public void commit()
        throws Exception /*keep example simple*/{

        _space.write(_nd,_txn,Lease.FOREVER);

    }
    public void abort()
        throws Exception /*keep example simple*/{
        _txn.abort();
    }
}

The EditableNodeData class makes writing client code simpler once we provide a method to return an instance.

public EditableNodeData editNodeData(
       NodeData node,long txnLen,long waitTime)
    throws Exception/*keeps example simple*/{

    Transaction.Created
             txnC=TransactionFactory.create(_txnMgr,txnLen);
    Transaction txn=txnC.transaction;
    NodeData tmpl=new NodeData(node._id,_treeName);
    NodeData nodeToEdit=(NodeData)_space.
                                    take(tmpl,txn,waitTime);
    //check for null and take appropriate action

    //else return EditableNode
    return new EditableNodeData(_space,txn,nodeToEdit);
}

The client code then takes the following form when used with the OrgTreeclass.

OrgTree orgTree=new OrgTree(/*params*/);

String [] path={"acme","sales","manager"};
long waitTime=1000;

OrgNodeData ond=orgTree.findByPath(path,waitTime);

long txnLen=1000;

EditableDataNode editable=
      orgTree.editNode(ond,txnLen,waitTime);

OrgNodeData data=
      (OrgNodeData)editable.getData();

//change the manager's name
data._name="J. Bond";
//update the entry
editable.commit();

Although we're only saving on two lines of code, we hope that this example provides another illustration of how wrapper classes can be used to hide underlying space-based operations.

Using the tree

One of the reasons for creating the organization chart system was so it would have the ability to start at the top and drill down to lower levels. We now have everything in place to be able to achieve this. Say, for example, we had to produce a web client for the system, and decided to use a servlet to generate the HTML (HyperText Markup Language). We could first produce a page with a hyperlink (on a generated image) with the information contained in the root node, and place the node's ID as a parameter in the hyperlink. That way, when the link gets clicked, the servlet can use the ID (in the parameter) and generate another HTML page with an image and links to all the children. This fulfills the drilling-down requirement.

Another requirement was for "static" links to points in the hierarchy which we have implemented in terms of the findByPath() method in OrgTree. With servlets, we could create a hyperlink that called a servlet to display the chart for a known point in the hierarchy. An example hyperlink may look like this:

/servlet/OrgChart?gen=yes&path=sales-paris-managers

Both the drill-down and find-by-path techniques could equally well be used by a Java application using Java Foundation Classes (JFCs).


Summary

In this chapter we have seen how hierarchical data structures can be used with spaces. JavaSpaces provides us with a medium to store large trees that can be edited and updated without the need to 'lock' or transport the whole data structure. By using transactions on takes and sensible timeouts on reads we can avoid deadlocks when one client is trying to update a subtree and another client is trying to delete part of it.

We examined two different techniques for defining a tree in terms of entries. A bespoke tree such as the SimpleNode has advantages over a generic tree like DistributedTree, in that it produces half the number of entries in the space, whereas the more generic DistributedTree has the advantage of being reusable for unnamed nodes, and it also separates the nodes from the associated data – meaning that the data aren't serialized with every node.

Locating subtrees within a hierarchy requires up-front analysis of the usage patterns. We have seen how to recursively walk trees to find a node, but this is an expensive operation on large trees. Adding key fields such as a path assists with matching, but makes renaming or moving nodes more complex and slower.

Wrapper classes provide a way to encapsulate space-based operations, making client code easier to write and providing a way to maintain data integrity when relationships between the entries making up the underlying data structure become complex.


Part II: Mechanics

    Chapter 6. Codebases

    Chapter 7. Remote event-based idioms

    Chapter 8. Code mobility –behavior-based idioms

    Chapter 9. Using transient and persistent spaces


Chapter 6. Codebases

As we have seen, JavaSpaces, along with all other Jini services, can dynamically publish proxies that are loaded into clients of Jini services at runtime via Jini's lookup service. In order to make this happen, both Jini services and Jini clients use Java's remote method invocation (RMI) dynamic classloading mechanism. In the next two chapters, we will be looking at remote events and code mobility but before we do so, it is important to understand how the underlying RMI mechanisms that support these features work.

In our experience, many of the initial problems with developing both RMI and Jini services are a result of not understanding how this mechanism operates, and hence it becomes very hard to figure out where to look when things go wrong.

In the first part of this chapter, we are going to take a look at the steps you need to take to configure your environment and programs to get everything up and running properly.

If this information is new to you, it may appear a little heavy going, and so we have also provided a very simple example of a notify tester later on, along with a simple configuration checklist.


Basic requirements

You need three things to be set up correctly for RMI to be able to dynamically download Serializable classes:

  1. An RMISecurityManager installed and running within the JVM.

  2. A properly configured java.rmi.server.codebase property.

  3. An HTTP server configured and running.

We will look at each of these three items in turn shortly, but first it's worth recapping how objects get serialized to understand why codebases are necessary.

When you serialize an object via an ObjectOutputStream, the object is converted into a byte array, which can later be deserialized via an Object InputStream. If the serialized object were to be deserialized by another process that didn't have access to the serialized object's class or any classes it references, a ClassNotFoundException would result. RMI resolves this problem by adding information to the serialized object in the form of a location where the classloader can look to find the class(es) associated with the serialized object; that is, if it doesn't have access to them already.

When this information is added, the serialized object is said to be "annotated" with the codebase information. The RMI subsystem has a specialized classloader, the RMIClassLoader, which can make use of the annotated information to resolve any classloading problems.


The codebase system property

Codebases are set by giving a value to the java.rmi.server.codebase system property. The java.rmi.server.codebase property is expected to be in the form of a uniform resource locator (URL), which points to the "location" from where the class or JAR (Java Archive) files can be downloaded.

In almost all cases you should use an HTTP (HyperText Transfer Protocol) URL to specify the location. Of course this assumes that the classes are going to be served by an HTTP server located somewhere on the network. The syntax for an HTTP URL is:

http://<host:port>/<relative location>/<filename>

For example, if we were running an HTTP server (on its default port 80) on a host machine called SG1 and we wanted to download a JAR file (printer- dl.jar) in a directory called print-service off the root directory, the URL would be as follows:

http://sg1/print-service/printer-dl.jar

HTTP assumes that if no port is specified in a URL, then the default port 80 should be used. If, on the other hand, we were running the HTTP server on port 6060, then the URL above would change to include the port number:

http://sg1:6060/print-service/printer-dl.jar:

Setting the codebase

The java.rmi.server.codebase property is set in exactly the same way as all the other Java systems properties. You can set properties in one of two ways. The first option is to set the property in the Java code that you are running. For example, to set the codebase property to the value above, we would use:

String theURL="http://sg1:6060/print-service/printer-dl.jar";
System.setProperty("java.rmi.server.codebase",theUrl);

If you are going to use this method, remember to set the codebase property early on in the code before any serialized objects are transported, or the RMI classloader will not know where to look to find the classes for the serialized objects it is receiving.

The other method is to set the codebase using the –D switch when starting the Java virtual machine on the command line.

java -Djava.rmi.server.codebase=http://sg1:6060/print-
service/printer-dl.jar <class with main>

The second approach is the preferred method of setting the codebase property, as it is more flexible. This is because you can easily write scripts to substitute the host name, port, etc., making the program's startup scripts portable to different hosts with different configurations.

There are three types of codebase syntax to specify the URL to point to:

  1. Class files.

  2. A single JAR file.

  3. More than one JAR file.

Specifying class files

To specify a location where class files reside, the URL must have a trailing forward slash as shown below.

-Djava.rmi.server.codebase=http://myhost/myService/

Specifying single JAR file

To specify a single JAR file, append the JAR file name to the end of the URL containing the directory (or alias):

-Djava.rmi.server.codebase=http://myhost/myService/myService.jar

Specifying multiple JAR files

To specify multiple JAR files, separate a list of URLs with spaces and enclose them in quotation marks. On Windows platforms, these must be double quotes and on UNIX platforms they must be single quotes.

On Windows:

-Djava.rmi.server.codebase=
  "http://myhost/myService/aService.jar http://myhost/utils.jar"

On UNIX:

-Djava.rmi.server.codebase=
  'http://myhost/myService/aService.jar http://myhost/utils.jar'

MarshalledObjects

The java.rmi.server.codebase property is also required when using MarshalledObjects, which are used extensively in Jini. MarshalledObjects provide a convenient way of wrapping a Serializable object to pass from client to client without the need for the recipient to deserialize the underlying object. The main advantage of this is that the recipient of the MarshalledObject doesn't have to download any classes that the underlying Serializable object is dependent on – unless that is, get() is invoked on the MarshalledObject.

Under the hood, a MarshalledObject serializes an object into a byte [] along with the java.rmi.server.codebase property, which may be needed later by the RMIClassLoader to download classes referenced by the serialized object. This means that when a MarshalledObject is deserialized, the underlying object is not, and hence only the MarshalledObject class is required, not the class for the internally serialized object.

The HTTP server

As we have said, the other side of using HTTP codebases is that you require an HTTP server running on the network in order to "serve up" the required classes or JARs. If you already have an enterprise web server running on the host that you're developing on, then you should use it to create aliases for the directories in which your class or JAR files live.

Aliases are created by defining a name (e.g. printservice) and mapping that name to a directory on the host's hard drive (e.g. c:\services\printer). So if we had the printer-dl.jar residing in a printservice alias, the resulting URL on host SG1 would be http://sg1/printservice/printer-dl.jar.

Alternatively you could create subdirectories off the root directory used by the web server to achieve the same effect.

If you are using class files, not JAR files, then you should avoid having one single directory hierarchy either off the root or assigned to a single alias that contains classes for all your clients and services. This is because conflicts can easily occur between different class versions.

If you don't have an enterprise web server running on your development machine, you can download a lightweight web server, implemented in Java, from the book's website. The lightweight web server supports aliases that can be configured either via a GUI (Graphical User Interface) tool that is also available from the website or by manually editing a properties file.

The RMI security manager

The final piece of the jigsaw you need to get everything up and running is to set the system security manager to be the RMISecurityManager. This is done in order to permit the RMI classloader to do its class resolution magic.

Here is the generic code required to set the JVM's security manager to the RMISecurityManager.

if (System.getSecurityManager()==null){
    System.setSecurityManager(
          new RMISecurityManager());
}

That is all you need. With all these parts in place, objects can roam freely across the network and clients can find the classes that they need in order to recreate the objects dynamically in their local JVMs.

A NotifyTester

As a very simple example, assume that you have written a space-based client called NotifyTester which registers for remote events, and you have already compiled the remote listener's stub.

The first thing to check before you test the client is that you have written the code to set the system security manager to use the RMISecurityManager, as shown above. The next step is to set up an alias on your web server to point to the directory where the stub and other classes that may need to be downloaded reside. So, let's assume that the location of this directory is c:\dev\spaces\notify and you have created an alias called "notify" to point at this directory.

Once you have an alias set up (and checked that the web server is running) you can specify the codebase property in the startup script shown below, and then run your client.

java –Djava.rmi.server.codebase=http://SG1/notify/ NotifyTester

If you have done this as set out, you should not have encountered any problems, but if you have, use the following checklist to make sure you have everything running and configured properly.

  1. Check that your code sets the system security manager to use the RMISecurityManager .

  2. Check that the codebase property has been set – if you are using the command line –D switch, then check the script that started the program.

  3. If the codebase property has been set then check that you have a HTTP daemon or web server running on the correct port.

  4. If the HTTP daemon is running and you have specified a JAR file in the codebase URL, then try typing the codebase URL into a web browser. If everything is configured correctly, the browser should prompt you to download the file; if not you will get an HTTP error.

  5. If you get an error back from the web browser, then recheck the codebase and that the HTTP daemon is configured correctly for the specified codebase.

A simple utility that we have also found useful has been to use a method to check that the codebase property has been set. By using the code snippet shown below, you will be alerted if you forget to set the codebase or your script becomes corrupt, etc.

public static void checkCodebase(){
   String codebase
       =System.getProperty("java.rmi.server.codebase");

   if(codebase==null){
      String msg="Codebase not set. Use"+
          "-Djava.rmi.server.codebase=";

      throw new RuntimeException(msg);
   }
}

If you use this technique, then make the call to checkCodebase() the first line in main().

Codebase RMI tips

Here is a list of rules that we have accumulated from experience and other sources, that you might like to review from time to time. We find them very useful in guiding us when constructing and configuring RMI-based systems.

  • Never use a file URL.

  • Never use localhost in a URL.

  • Use scripts to obtain the host name (and other parameters from environment variables) and substitute them at runtime.

  • Create downloadable JAR files, because they are smaller and faster than loading individual classes.

  • Don't put all your project classes in a downloadable JAR – only include the classes that need to be downloaded (jarfilename-dl.jar).

  • Reduce the number of HTTP ports in use and the number of HTTP servers by using a single HTTP server.

  • Use either aliases or directory structures to separate different projects' JAR or class files.

  • Use the same naming policy for aliases in development as used in production to avoid unnecessary configuration problems later.

  • Avoid having class files for more than one project in the same location.

  • Avoid using the web server's root directory.

  • If you want to specify classes rather than JAR files in a codebase, the URL must have a trailing forward slash.

  • Specify multiple JAR files by delimiting URLs with spaces.

  • Make sure you have enclosed delimited multiple JAR files in the correct quotation characters for the platform you are using (Windows or UNIX.)


Summary

The Java RMI subsystem includes the ability to dynamically load new classes that correspond to serialized objects, which may be received while Java RMI programs are running. There are several configuration steps required to make sure that clients have access to these classes. We have shown the steps required to configure a system to make these classes available, and to configure clients so that they can take advantage of the underlying RMI facilities.

Use the checklist provided to ensure that your system is configured correctly, and refer to the rules of thumb listed in this chapter. The good news is that if you stick to these rules, once your systems work, they tend to stay working.


Chapter 7. Remote event-based idioms

The distributed event model used by JavaSpaces is defined in the Jini Technology Core Platform Specification. The specification defines the interfaces and requirements placed on a Jini service, such as a JavaSpace, which is capable of sending remote events. The JavaSpaces specification "overrides" some of the core Jini specification, and provides more details on how a JavaSpace implements remote events.

The remote event model allows an object in one JVM to register an interest in some event occurring in an object (possibly) in another JVM, and receive notification of when that event occurs. For example, a JavaSpace can send remote events when an entry matching a specified template is written into the space.

In this chapter we will be examining space-based designs and idioms that utilize the JavaSpace implementation of the Jini remote event model.


Introducing remote events

So far, in this book we have examined designs that use a polling/blocking strategy to read or take entries from the space. An example of this strategy was used in Chapter 4 by the alerter process, which sent e-mail and SMS alerts when another Jini service encountered an error. The service that encountered the error wrote a ServiceError entry to the space.

The polling/blocking strategy is generally used so we don't miss any new entries that are written to the space – recall that it is possible (due to operation ordering) to miss an entry on a blocking take if the take() occurs on or at about the same time as a write().

Below is a quick reminder of how the polling/blocking strategy code looked in the alerter process.

long waitTime=1000;
ServiceError tmpl=new ServiceError();

while(Thread.isInterrupted()==false){

   //try to take the entry within a specified time
   ServiceError serviceError=space.take(tmpl,null,waitTime);
   //if an entry exists process it, else try again
   if(serviceError!=null){

      //process error and send alerts
   }
}

A polling strategy is good in that it is reliable. However, it is expensive in terms of network traffic and processor cycles. An alterative approach is to use the remote event mechanism provided by a JavaSpace. We will look at how to modify the alerter system to take advantage of remote events later, but first we need to take a look at how to use the JavaSpaces notify() method.

The choice of method name is somewhat unfortunate, as it is also the name of the method that remote event listeners implement, albeit with different parameters. If you are already familiar with the Jini remote event model, but are new to JavaSpaces, you may find this confusing to begin with.

Notify

The JavaSpace interface defines a method called notify(), which allows you to register for remote events based on a template; for example, to receive a remote event when a new ServiceError (entry) is written to the space, we would pass an empty ServiceError instance (to act as the template) to the notify() method.

EventRegistration notify(Entry tmpl, Transaction txn,
      RemoteEventListener listener,
      long lease, MarshalledObject handback)
        throws TransactionException, RemoteException;

The normal JavaSpaces matching semantics apply for template entries to notify(), so in the case of an empty ServiceError entry being used as a template, remote events would be sent each time a ServiceError is written to the space, regardless of the values its fields have been set to.

RemoteEventListener

We will look at the other parameters to notify shortly, but for now let's concentrate on what we need to do to receive remote events. First, we need to write a class that implements the RemoteEventListener interface, which is defined as follows.

package net.jini.core.event;

public interface RemoteEventListener
  extends java.rmi.Remote, java.util.EventListener{
  void notify(RemoteEvent theEvent)
     throws UnknownEventException,
            java.rmi.RemoteException;
}

As you can see, the RemoteEventListener interface defines a single method called notify() hence the potential confusion (mentioned earlier) with the two distinctly different types of notify() method. Figure 7.1 shows the interaction with the space.

Figure 7.1. Remote events

Classes that implement RemoteEventListener must be RMI remote objects; that is, they must either be an Activatable or an UnicastRemoteObject. This is so the space can invoke the RemoteEventListener.notify() method on an object running in a different JVM.

There are several ways to make an object remote, but for this example we will use the simple technique of making our event listener a subclass of Unicast RemoteObject. One interesting point to note here is how we get hold of the proxy to the space in this example. Instead of using the SpaceLocator utility class, we obtain a reference to the proxy from the RemoteEvent via getSource().

import net.jini.core.event.*;
//helper classes
import jsip.jini.SpaceLocator;
import jsip.jini.TxnManager;
public class ServiceErrorListener
    extends java.rmi.server.UnicastRemoteObject
    implements RemoteEventListener{

    public ServiceErrorListener()
        throws java.rmi.RemoteException{
            super();
    }

    public void notify(RemoteEvent theEvent)
       throws UnknownEventException,
              java.rmi.RemoteException{

       Transaction txn=null;
       JavaSpace space=(JavaSpace)re.getSource();

       try{
          txn=TxnManager.createTxn(60000L);

          long waitTime=500;
          ServiceError tmpl=new ServiceError();

          //try to take the entry within a specified time
          ServiceError serviceError=
             space.take(tmpl,txn,waitTime);

          //if we get it then process it
          if(serviceError!=null){
             //process error and send alerts
             txn.commit();
         }else{
             txn.abort();
         }
       }catch(Exception ex){
             txn.abort();
          //handle exception & rethrow
       }
    }
}

Looking at the above code, you will see that the ServiceErrorListener class extends the UnicastRemoteObject, which will allow instances of the class to be contactable from other JVMs via RMI. ServiceErrorListener also implements RemoteEventListener so it can receive remote Jini events.

The constructor in ServiceErrorListener invokes super() to initialize the UnicastRemoteObject. It's worth pointing out that a JVM will not exit while a UnicastRemoteObject is active, so for a graceful shutdown you should always deactivate UnicastRemoteObjects by invoking the static method UnicastRemoteObject.unexportObject(Remote obj, boolean force).

Finally, ServiceErrorListener declares the implementation of the notify() method that is invoked when a matching entry (ServiceError) is written to the space, meaning (in the current context) that we can attempt to take() that entry and send e-mail alerts to all interested parties.

TIP

Return from notify() as quickly as possible.

At first glance, the strategy of taking (or reading) an entry as the result of a notify() may seem a good one. However, it isn't very kind on the space or other remote listeners, as the thread performing the notification (the distributed callback) is blocked for the duration of the call. The chances are that most space implementation will use a pool of threads to perform notifications, so that if several remote listeners spend a significant amount of time in notify() mode, other listeners would have to wait until a thread from the pool becomes available. In light of this, we recommend returning from notify() as quickly as possible.

The simplest technique for returning from notify() as quickly as possible is to create a separate thread to handle the event. However, this results in many thread objects being created and destroyed. Another approach is to have a background thread that uses a queue of events populated by notify(). This way, notify() can return as soon as the event is added to the queue, allowing the background thread to process the event at its leisure.

public void notify(RemoteEvent theEvent)
     throws UnknownEventException,
            java.rmi.RemoteException{

     _queue.add(theEvent);
}

As this is a strategy we want to use in most of our programs, it's worth making the effort to write a reusable class. Below is the listing for a reusable remote event listener that queues events for subclasses to process. The source code for QueuedEventListener and Queue are available from the book's website and are listed in Appendix A.

package jsip.notify;

import java.rmi.server.UnicastRemoteObject;
import net.jini.core.event.*;
public abstract class QueuedEventListener
    extends UnicastRemoteObject
      implements RemoteEventListener, Runnable{

    private Queue _queue=new Queue();

    public QueuedEventListener()
        throws java.rmi.RemoteException{
            super();
            Thread t=new Thread(this);
            t.start();
    }
    public void notify(RemoteEvent theEvent)
      throws UnknownEventException, java.rmi.RemoteException{

      _queue.add(theEvent);
    }
    //subclasses do the actual processing at their leisure
    public abstract void processEvent(RemoteEvent evt);

    public void run(){
        while(true){
            try{
                processEvent((RemoteEvent)_queue.getNext());

            }catch(InterruptedException ex){
                return;
            }
        }
    }
}

Now that we have a reusable framework for queuing events, the Service ErrorListener can be rewritten to simply implement the processEvent() method which is declared abstract in QueuedEventListener. As events arrive, QueuedEventListener adds them to a queue and returns. The run() method then blocks on _queue.getNext() until an event arrives, after which it invokes processEvent().

import net.jini.core.event.*;
import java.rmi.RemoteEvent;
import jsip.notify.QueuedEventListener;

public class ServiceErrorListener extends QueuedEventListener{

  public ServiceErrorListener()
     throws RemoteException{
      super();
  }
  //the code to process an event
  public void processEvent(RemoteEvent re){
    //same code as the first
    //ServiceErrorListener.notify()
  }
}

The above technique works well as it fulfills our requirement to return from notify() as quickly as possible: however, as usual, there is a trade-off. This time the trade-off is that we can't (if we needed to) throw an Unknown EventException in notify() as the events are queued. The Jini Remote Event Specification allows listeners to throw an UnknownEventException if they receive an unknown event, allowing the caller to remove the listener from its list of EventRegistration objects. In practice, this is an unlikely scenario, so the trade-off has more pros than cons.

Note on remote objects

Making objects remote by having their class extend UnicastRemoteObject is pretty straightforward; however, all remote objects require an associated stub, which isn't generated automatically by the Java compiler. Some IDEs do this automatically for you when you build your project, which makes life easier, but if you're not using an IDE or one that supports this feature you need to run the RMI Compiler (RMIC) tool on the class file to produce the stub.

To create a stub for the QueuedEventListener use the following command line where the –d flag specifies the directory to write the stub (and skeleton) to. We only need to compile the stub and skeleton for QueuedEventListener, not for each subclass.

rmic -d . jsip.notify.QueuedEventListener

Running the above command line produces the following files:

QueuedEventListener_Stub.class
QueuedEventListener_Skel.class

The _Stub class will be downloaded by the space and the _Skel class used by the client that registered the remote event listener.

The above use of the RMIC generates stubs and skeletons that are compatible with Java 1.1. However, if you use the –v1.2 switch no skeleton will be produced. This is because from Java 2 onwards a dynamic skeleton is generated which uses reflection to invoke remote methods.

Setting up codebases and an HTTP server to facilitate RMI's dynamic classloading is required to get everything working. This can be tricky until you get to grips with the concepts involved – refer back to Chapter 6 for more details. If you're already up to speed on codebase issues, then all you need to do is make sure that the stub is available for downloading by the space.

Notify parameters

In the previous section we discussed two of the parameters to the JavaSpaces notify() method – the Entry template and the RemoteEventListener. Notify takes three additional parameters; a Transaction, a lease length, and a MarshalledObject. We will look at the lease length and the Transaction parameters in their own sections later, but let's look at the MarshalledObject handBack parameter now.

MarshalledObject – handBack

MarshalledObject was introduced in Java 2 and is used extensively in Jini as it provides a convenient way of wrapping a Serializable object to pass over the wire without the need for the recipient to deserialize the underlying object. The main advantage of this is that the recipient of the MarshalledObject doesn't have to download any classes that the underlying Serializable object is dependent on – this is great news for services such as the Jini Lookup Service, and for services that store "cookies" for clients, such as the handBack parameter to notify().

Under the hood, a MarshalledObject serializes an object into a byte [] along with its java.rmi.server.codebase property, which may be needed later to download classes referenced by the serialized object. This means that when a MarshalledObject is deserialized the underlying object isn't deserialized, as it's held as a byte []. MarshalledObject also provides a useful implementation of the equals() methods, which allows you to compare two MarshalledObject underlying byte arrays without taking their codebases into account.

A MarshalledObject is instantiated by passing a Serializable object into its constructor.

String toMarshall="marshall me";
MarshalledObject mo=new MarshalledObject(toMarshall);

The object can later be retrieved by calling get() on the MarshalledObject and casting the return value to the type you expect – this is when the underlying object gets deserialized.

String unMarshalled=(String)mo.get();

Let's now turn our attention back to notify() and what the Marshalled Object parameter is used for. The object passed as the MarshalledObject parameter to notify() is returned and assigned to a field in the RemoteEvent, and is accessible via the method RemoteEvent.getRegistrationObject(). This allows context information to be passed back to the remote event listener.

An example where this context information could be used is when a single remote event listener has been written to handle multiple events in the same way that an ActionEventListener may be used in an AWT (Abstract Window Toolkit) or JFC (Java Foundation Classes) program to process events from multiple components such as buttons. An unfortunate side-effect of this technique is the distinctly un-OO use of switch-type statements to determine which action to take. The example below shows this technique in action.

public void notify(RemoteEvent theEvent)
  throws UnknownEventException,
         java.rmi.RemoteException{

  MarshalledObject mo=
      theEvent.getRegistrationObject();

  Integer context=(Integer)mo.get();
  switch( context.intValue() ){
     //switch on defined constants
     case EVENT1 : {
          //handle event type
          break;
     }//etc
  }
}

The trade-off with this technique is a reduction in the number of remote event listener classes against a potential maintenance problem in the form of switches or a series of if/else clauses.

Reducing the overall number of remote event listener classes is an admirable objective which can be achieved in another way without the introduction of switch statements – we refer to this technique as the "handback event handler" idiom.


The handback event handler

Another more interesting usage of the MarshalledObject handBack parameter is to pass in an object that knows how to handle the event type you are registering an interest in. This means that we only need to declare a single remote event listener and register it multiple times with different templates and handBack parameters. It is important that we remember to be careful about the serialized size of the object used as the handBack parameter, otherwise the trade-off will not work in our favor.

The Handler interface (shown below) defines a single method handleEvent(). Objects that implement this interface will be used to pass the handBack parameter.

//generic interface
public interface Handler extends Serializable{
  void handleEvent(RemoteEvent re);
}

In the RemoteEventListener 's notify() method the MarshalledObject is unpacked, cast to the Handler interface, and the RemoteEvent passed on to the Handler instance.

//the one and only event listener
public void notify(RemoteEvent re)
  throws UnknownEventException,
         java.rmi.RemoteException{
  try{
    MarshalledObject mo=re.getRegistrationObject();
    Handler h=(Handler)mo.get();
    h.handleEvent(re);
  }catch(Exception ex){
     //handle exception
  }
}

This idiom has several interesting implementations, the most effective of which uses dynamic classloading. For example, if we define a concrete Handler as a simple wrapper around a String field that has the implementation class name, we can dynamically add new handlers for event types at runtime while also keeping the size of the MarshalledObject to a minimum – the size of a String object's representation of the class name.

The DynamicHandler class below illustrates how to define a generic handler that dynamically loads the implementation class based on the _implClass field.

public class DynamicHandler
  implements Handler{

    private String _implClass;
    public DynamicHandler(String implClass){
       _implClass=implClass;
    }

    public void handleEvent(RemoteEvent re){
      try{
         Class impl=Class.forName(_implClass);
         Handler h=(Handler)impl.newInstance();
         h.handleEvent(re);
      }catch(Exception ex){
         //handle exceptions
     }
  }
}

Building on the QueuedEventListener we can create a subclass to process the events as described above. The EventHandlerListener becomes the only remote event listener implementation that we register (possibly multiple times) with the space. Classes that implement the Handler interface do the actual event processing.

package jsip.notify;

import net.jini.core.event.RemoteEvent;
import java.rmi.MarshalledObject;
public class EventHandlerListener
  extends jsip.notify.QueuedEventListener{

    public EventHandlerListener()
        throws java.rmi.RemoteException{
        super();
    }
    public void processEvent(RemoteEvent re){
        try{
          MarshalledObject mo=re.getRegistrationObject();
          Handler h=(Handler)mo.get();
          h.handleEvent(re);
        }catch(Exception ex){
        jsip.debug.Debug.message(ex);
        }
    }
}

Having defined the remote event listener we are going to use, we can then define the classes that do the actual event processing. Below is an example ServiceErrorHandler implementation.

import net.jini.core.event.RemoteEvent;
import net.jini.space.JavaSpace;
import jsip.jini.TxnManager;

public class ServiceErrorHandler
    implements jsip.notify.Handler{

    public void handleEvent(RemoteEvent re){
        //same code as the original
        //ServiceErrorListener.notify()
    }
}

The UML class in Figure 7.2 shows the relationships between the classes used by the handback event handler idiom.

Figure 7.2. Handback event handler

Finally we can take a look at the code to register and use the EventHandler Listener, ServiceErrorHandler, and DynamicHandler classes.

JavaSpace space=SpaceLocator.getSpace();

//instantiate the one and only listener instance
RemoteEventListener listener=new EventHandlerListener();

//template to match on
ServiceError tmp=new ServiceError();
//only notify for 1 minute

long lease=ONE_MINUTE;
//no transaction, so use null
Transaction txn=null;

//configure the DynamicHandler and
//create the MarshalledObject handback
String handlerClass= "ServiceErrorHandler";
DynamicHandler handler=new DynamicHandler(handlerClass);
MarshalledObject mo=new MarshalledObject(handler);

//register for events
EventRegistration er=space.notify(tmpl,txn,listener,lease,mo);

//assume we have another Handler class MessageHandler
//that processes MessageEntry object

MessageEntry msgTmpl=new MessageEntry();
handlerClass= "MessageHandler";
handler=new DynamicHandler(handlerClass);
mo=new MarshalledObject(handler);

//register the same listener with a different template
// and different handback
EventRegistration
  er2=space.notify(msgTmpl,txn,listener,lease,mo);

Looking at the code you can see that we can register the same EventHandler Listener instance multiple times with different templates and different MarshalledObjects. The interactions between the various classes are quite subtle and are described below.


Object interactions

  1. The space calls notify() on the EventHandlerListener which is a subclass of QueuedEventListener.

  2. The QueuedEventListener queues the event and returns.

  3. A thread in QueuedEventListener takes the event from the queue.

  4. Each event is passed to the processEvent() method in EventHandler Listener.

  5. The processEvent() method then extracts the "handback". Marshalled Object (from the RemoteEvent) which contains the Handler (Dynamic Handler) and passes the event to the handler for processing.

  6. The DynamicHandler.handleEvent() method then loads the class the name of which is stored in the _implClass field, and passes the event to its final destination – the ServiceErrorHandler.


Adding event handlers at runtime

Earlier we mentioned that this idiom could be used to dynamically load and register new handlers at runtime. There are various techniques we could use to achieve this, the simplest of which is to specify a handler class name and entry template class name in a properties file, and run a thread which occasionally checks to see if the file has been modified. If the properties file has changed, it can then be reloaded and parsed to see if any new handler/template pairs have been added. The properties file could look something like this:

# Handler class name=template class name
ServiceErrorHandler=ServiceError

The template and handler classes would then be instantiated by using Class.forName() and newInstance(), and a DynamicHandler created in the same way as the code listing above illustrated. A more advanced approach is to provide a codebase in the properties file and then use the RMIClassLoader to load the class.

The problem with our approach thus far is that it will only work for empty entry templates; that is, we can't set any key field values that we might require for matching. A more sophisticated technique could use a serialized entry as the template stored in a file. The properties file could then specify the file name the serialized template is stored in.

# Handler class name= serialized template file name
ServiceErrorHandler=/templates/serviceerror.ser


Leases

One of the parameters of JavaSpace's notify() method is a lease length in the form of a long value representing the duration in milliseconds you want the space to keep sending remote events to the RemoteEventListener. The lease time you specify may or may not be granted by the space.

TIP

Never assume that the space will grant you the lease length you request.

The lease time granted by the space is held as a Lease in the Event Registration object returned by notify().


Event registration

The JavaSpace notify() method returns an EventRegistration object which contains, among other things, the lease time granted by the space for the RemoteEventListener. For this reason alone you should never ignore EventRegistration objects returned by notify().

TIP

Never ignore the EventRegistration object returned by notify() .

The Lease object is accessible via the getLease() method on the EventRegistration object, and should be passed to a lease renewal class to guarantee that the space doesn't dispose of your RemoteEventListener. Failure to keep renewing the lease will result in no further events being sent to the RemoteEventListener when the lease expires.

TIP

Always renew leases until you have finished with a resource.

Jini has a LeaseRenewalManager class (in the net.jini.lease package) that you can use to renew the lease returned in the EventRegistration object.

//should be a single reused instance
//don't create a LeaseRenewalManager for each Lease
LeaseRenewalManager lrm=new LeaseRenewalManager();

//register for events
EventRegistration er=space.notify(/*params*/);

//get the Lease object
Lease lease=er.getLease();

//pass to lease renewal manager
lrm.renewUntil(lease,ONE_HOUR,null);

The EventRegistration class has three other methods.

  1. public long getID(), which returns the identifier that will be used in all RemoteEvents generated for this registration.

  2. public Object getSource(), which will return a reference to the JavaSpace.

  3. public long getSequenceNumber(), which returns the value of the sequence number that was current when the registration was granted, allowing comparison with the sequence number in any subsequent RemoteEvents.


Sequence numbers

Each RemoteEvent received by a RemoteEventListener contains a sequence number, which is a monotonically increasing number. For each RemoteEvent Listener a new matching entry will cause the space to increment the sequence number. Well, that's how it used to be, and will still work in practice with a transient space; however, the JavaSpaces specification has changed to allow persistent spaces to increment a sequence number by a large delta value after recovering from a crash.

Sequence numbers are accessible via both the RemoteEvent's and Event Registration's getSequenceNumber() methods.

Let's look at an example that's applicable to a transient space, such as Outrigger. If the last RemoteEvent received by a RemoteEventListener had a sequence number of 5, and the subsequent RemoteEvent had a sequence number of 7, we can determine that 2 (7 – 5) matching entries have been written to the space between the two events. Keep in mind that it is possible that the RemoteEventListener may not have received the intermediate event, due to network failure or due to event compression being performed by the space.

Sequence numbers are local to a particular event registration; that is, you cannot deduce the total number of matching entries that have been written to the space since the space started up. It is possible to achieve this, but you would need to register a remote event listener before any matching entries have been written, and you must ensure that you renew its lease.

As a general rule, to determine the total number of matching entries written to the space since registering for events via notify(), take the most recent sequence number and then deduct the sequence number returned in the EventRegistration object. We say this is a general rule, because a space might return the first RemoteEvent sequence number equal to the sequence number that is returned in the EventRegistration object, in which case you would need to add 1 to the total.

The example code below builds on the QueuedEventListener class we developed earlier, and simply prints out the total number of matches since registering with notify() each time a remote event is received.

import net.jini.core.event.RemoteEvent;

public class SequenceNumListener
    extends jsip.notify.QueuedEventListener{

    private long _startSeqNum;

    public SequenceNumListener()
        throws java.rmi.RemoteException{
        super();
    }
    public void setStartSeqNum(long num){
        System.out.println(num);
        _startSeqNum=num;
    }
    public void processEvent(RemoteEvent re){

       long numMatches=
           re.getSequenceNumber()-_startSeqNum;

       System.out.println(
             "Number of matching writes="+numMatches);
    }
}

The code below shows how to set up the listener:

SequenceNumListener snl
   =new SequenceNumListener();

EventRegistration er=
   space.notify(tmpl,null,snl,leaseLen,null);

seqNumListener.setStartSeqNum(er.getSequenceNumber());

The SequenceNumListener class could easily be modified to store the current count rather than print it. That way, another class could query the SequenceNumListener via an accessor method to determine the total number of matching entries since registration.


Transactions

Registering for event notification (via notify()) with a Transaction allows you to receive remote events when matching entries are written to the space in the same scope as that transaction. This means that the registered Remote EventListener will receive events for matching entries written to the space under that transaction, as well as matching entries written under a null transaction – entries written under null transactions are visible immediately, as they are implicitly committed.

A RemoteEventListener registered with a null transaction will only receive events when matching entries written under transactions have been committed (thus becoming visible), or when entries are written under a null transaction.

Finally, it's worth pointing out that as sequence numbers are local to an event registration, a RemoteEvent triggered under a null transaction but received by a listener registered under a transaction may have a different sequence number than a RemoteEvent received by another listener listening under a null transaction. This can occur even though the same write triggered both events.


Leases and transactions

Earlier we advised that you should always renew leases. In the case of transactions you need take care to renew the correct lease (should you wish to extend the transaction's lifespan).

A Transaction's lease and an EventRegistration's lease relate to different resources. This means that renewing the lease object returned with the EventRegistration object will not renew the Transaction's lease, and vice versa. When a transaction commits or aborts, the EventRegistration becomes invalid. Attempts to renew a lease after expiry will result in an UnknownLeaseException. However, having said that, you will probably want to be considered as a good Jini citizen, in which case you should renew the EventRegistration's lease until you either commit or abort the transaction, at which point you should cancel the EventRegistration's lease.


Using an Event Mailbox

Thus far we have only considered processing events as they occur in our space-based client code. This is mainly because the alerter system we have been developing is required to send timely e-mail and SMS text messages when another Jini service encounters an error. There is another technique we can use for space-based clients that don't need to process events in a timely fashion – by using an Event Mailbox service, such as Sun's EventMailbox implementation (called Mercury) that comes with the Jini starter kit.

An Event Mailbox service stores events on behalf of a client until that client requests delivery of those events. This is analogous to the way your Internet Service Provider (ISP) stores your e-mail until you log in and request it.

The EventMailbox service can be useful to space-based clients who don't need to process events as they occur. We could modify the Queued EventListener class we developed earlier to perform a similar task; however, the EventMailbox service provides us with resilience in the event of partial network failure, or if our client crashes. That is, a properly written client of the EventMailbox service can crash or elect to shut down without missing events. When the client starts up again (or is activated via RMI's ctivation system) it can request delivery of any events that have occurred in its absence.

Using the EventMailbox service

Assuming we have an EventMailbox service running in our Jini community, we can locate the service by using our ServiceLocator class (in the jsip.jini package), which is available from the book's website.

import net.jini.event.*;
import net.jini.core.lease.*;

EventMailbox mailBox=
   (EventMailbox)ServiceLocator.getService(EventMailbox.class);

Once we have a reference to the service, we can register with it by specifying the length of time we would like the service to store events for us. Below, we request a lease duration Lease.FOREVER; however, as we mentioned earlier, there is no guarantee that a service will grant the requested lease duration, so we will have to be prepared to renew the lease – more about that later.

MailboxRegistration mailBoxReg=mailBox.register(Lease.FOREVER);

The MailboxRegistration object returned when we register with the EventMailbox contains the Lease object and a RemoteEventListener – this is the RemoteEventListener that we will pass to the space's notify() method.

RemoteEventListener boxListener=mailBoxReg.getListener();

The MailboxRegistration class also provides the interface to enable the delivery of events, which we will look at later on.

The next step is to register the listener with the JavaSpace's notify() method – assume we already have a reference to the space in the variable space and have created a template which is assigned to the variable tmpl.

EventRegistration er
   =space.notify(tmpl,null,boxListener,Lease.ANY,null);

Once the RemoteEventListener provided by the MailboxRegistration object has been registered with the space, our client code can happily go about its business until it is ready to process the events. At this point the client code enables event delivery by passing its own RemoteEventListener to the MailboxRegistration object.

mailBoxReg.enableDelivery(myListener);

To disable delivery, invoke the disableDelivery() method:

mailBoxReg.disableDelivery();

Finally, as we now have two registration objects, one for the space (EventRegistration) and one of the Event Mailbox (MailboxRegistration) we need to think about how we are going to renew their leases. If we don't care about crash recovery then we can use the LeaseRenewalManager to renew both leases for us as follows.

LeaseRenewalManager lrm=new LeaseRenewalManager();
lrm.renewUntil(er.getLease(),Lease.ANY,null);
lrm.renewUntil(mailBoxReg.getLease(),Lease.ANY,null);

On the other hand, if we want the EventMailbox to continue to receive and store events if our client crashes, using a local LeaseRenewalManager isn't an option. Instead we need to use another Jini service to renew leases for us – the Lease Renewal Service (LRS). A full description of this service is beyond the scope of this book but, in simple terms, as its name suggests, the Lease Renewal Service will renew leases, allowing a properly written client of the service to deactivate or crash and then restart without its leases expiring.

One final point to keep in mind is that for a client to recover from a crash and still be able to have its events delivered it must persist (possibly to local disk) the MailboxRegistration object so that it can reload it after a crash. If you use this technique, then don't forget to serialize the MailboxRegistration as a MarshalledObject to maintain its codebase.


Reliability

There are compelling arguments for designing systems that use distributed events. For instance, the ability to act on an event when it occurs as opposed to continuously polling a space to see if something has changed can reduce network traffic and the number of RMI threads used by the space. However, systems that use Jini-distributed events need to be written in such a way that they don't fail if they miss an event.

RULE

Jini and JavaSpaces remote events do not have guaranteed delivery.

Jini and JavaSpaces remote events are not guaranteed to be delivered; that is, a Jini service has no obligation to resend events if a network error occurs during delivery of an event.


Summary

Remote events allow us to write programs that can react when entries matching a registered template are written to the space, as opposed to using a polling strategy that uses up valuable resources such as network bandwidth and threads used by a JavaSpace.

Remote listeners should return from their notify() method as quickly as possible to avoid consuming too many notification threads in a JavaSpace, and possibly blocking other remote listeners from receiving events in a timely fashion. JavaSpaces notify() method returns a lease in the EventRegistration object. Failure to renew this lease will result in no further events being sent to the registered remote event listener after the lease expires.

Each remote event contains a sequence number, which can be used to determine how many matching entries have been written to the space since registration. Sequence numbers can also be used to determine if any events have been missed or whether the JavaSpace has compressed any events.

Persistent space may increment a sequence number by some large delta value after recovering from a crash – something you need to take into account if you rely on sequence numbers in your programs.

Registering for event notification with a Transaction allows you to receive remote events when matching entries are written to the space in the same scope as that transaction. This means that the registered RemoteEventListener will receive events for matching entries written to the space under that transaction, as well as matching entries written under a null transaction.

Jini's EventMailbox service can be used to store remote events for later delivery to another remote event listener.

Jini and JavaSpaces remote events are not guaranteed to be delivered; that is, a Jini service has no obligation to resend events if a network error occurs during delivery of an event.


Chapter 8. Code mobility –behavior-based idioms

Java's ability to move code as well as data is extremely powerful, and is fundamental to how both Jini and RMI work. The combination of Java interfaces, classloaders, and code mobility have spawned a wealth of idioms that can be used in many different types of system. Many of these idioms are beyond the scope of this book, but certain examples fit well with JavaSpaces technology, and this is what we will examine in this chapter.


Introducing code mobility

In Java, the term "code mobility" is used to refer to the JVM's ability to download the byte code (class files) it needs at runtime when it can't find them in the classpath. Typically the class files are in a JAR and located on a remote machine, but they could reside locally.

To introduce the concept of code mobility, let's consider what happens when you request a JavaSpace proxy from a Jini Lookup Service. You may simply request it based on the interface net.jini.space.JavaSpace with no additional attributes. If a proxy is present, it is downloaded and you can then start interacting with the space based on the interface. The good thing is that all this happens without your client having been deployed with the proxy's class files. It's worth noting that deploying your client with a known proxy's class files would be a bad thing, as that client build would be forever tied to that single implementation and could miss out on any future bug fixes or performance enhancements.


What goes on behind the scenes?

In simple terms, when a JavaSpace (or any Jini service) starts up it first discovers all the lookup services (LUSs) that support the group (or groups) the service wants to join, e.g "public". For each discovered LUS, the JavaSpace downloads the LUS's proxy and then registers its own proxy.

Before a proxy is serialized to the LUS it is first converted into a MarshalledObject which contains the serialized (proxy) object and the codebase property (java.rmi.server.codebase ) of the originating JVM. It is critical, as described in Chapter 6 , that this property has been set correctly to a valid URL. It is the serialized MarshalledObject which actually gets sent over the wire along with a list of interfaces supported by the (service) proxy, rather than a serialized version of the proxy. The motivation for using a MarshalledObject rather than the default RMI serialization mechanism is that the MarshalledObject can be deserialized in the LUS without it having to download all the proxy's classes.

When a JavaSpaces client starts, it discovers one or more LUSs that have the javaspace.class interface registered. It then downloads one or more JavaSpace proxies from the LUS. The JavaSpace proxy is retrieved from the LUS in its MarshalledObject form and reconstructed into a "real" object in the LUS's proxy running on the client. Under the hood, the default classloader will attempt to load that class and any classes it references. If the class has not been previously loaded, or the class cannot be found in the classpath, then the RMIClassloader will attempt to download the classes from the codebase. If for any reason the classes can't be loaded, a ClassNotFoundException will be thrown – for more information refer back to Chapter 6.

Figure 8.1 shows how a JavaSpace interacts with a Jini Lookup Service.

Figure 8.1. Interaction with a Jini Lookup Service

Although this may all sound a bit "low-level", later you will see how we can use this knowledge to write code that can dynamically update classes in a running JVM.

The ability to write code based on an interface and not care about the implementation is a fundamental principle of object-oriented design. Couple this with Java's code mobility, and then we can start building systems the components of which won't need to be taken down to update their behavior. Introduce JavaSpace technology into the equation, and some exciting design alternatives to traditional distributed systems emerge.

There are a few caveats associated with code mobility that we will address later in this chapter but, for now, just keep in mind the following.

  1. What happens when a JVM has already loaded one version of a class, and a newer version has become available?

  2. Can we trust unknown code that is downloaded?

  3. What happens if the client needing to download the classes can't contact the originator's machine?

As far as the third item is concerned, there isn't anything you can do about it except program defensively by making sure that any entries that you take from the space are taken under a Transaction – this is a policy you should always use. If a Transaction is not used with a take() an entry which references a class that the client can't load will be removed from the space before you find out that there is a problem. Once it is removed, you won't be able to put it back!


Introducing behavior-based idioms

In previous chapters, we have looked at using state-based idioms by writing Entry objects and using pattern matching to read or take those entries from the space. We call these "state-based" as we're not really dealing with any behavior (in the polymorphic sense); since the only methods that generally get called modify the state of the entry, rather than performing some task.

When we say "behavior-based" idioms, we are referring to entry objects that have polymorphic qualities and the subclasses of which may be unknown to the client – this is where code mobility comes into play.

Behavior-based entries often perform some task that results in another Entry, which gets written back to the space. Sometimes the motivation for this is to distribute the workload over several machines where the duration (or computational intensity) of the task is greater than the time it takes to take the Entry, and then write the resulting Entry back to the space – you can think of it as a kind of distributed load balancing. The most-documented pattern that uses this technique is the "Command" pattern.

Other behavior-based idioms can be used for dynamic code updates to provide 24 x 7 coverage, or to avoid having to take down part of a system when updates become available. The example used throughout this chapter falls into this category.


Building an employee system

The example we are going to use in this chapter is based on a simple employee system where employee records are stored in the space. Clients of the space use these records for various purposes, one of which is to calculate annual bonuses. The calculation of bonuses may change from time to time and vary depending on the grade of the employee. For this reason, we have defined an interface Employee with the method calculateBonus().

public interface Employee{
   public double calculateBonus();
}

Having defined the Employee interface, we now need to think about how we are going to design the Entry classes that implement it. The first thing to keep in mind is that we will need to be able to instantiate an entry to use as a template parameter to the space – this presents an interesting design decision when interfaces are involved and there is no default behavior for the entry. That is, if we were to declare an EmployeeEntry, as shown below, it would have to throw an exception in the calculateBonus() method because we can't provide a default implementation. This is because bonus calculations are grade-specific.

import net.jini.core.entry.Entry;

public class EmployeeEntry implements Entry, Employee{
   public Integer empNo;
   public Serializable empData;
   //accessors
   public double calculateBonus(){
      throw new UnsupportedOperationException();
   }
}

The above approach is fairly common in entry design, and is the one we are going to use in this example.

There is an alternative approach, which we examined in Chapter 2, but to save you flicking back here's a quick reminder of the technique applied to the current discussion. If we make EmployeeEntry a simple entry with no behavior, we could then have subclasses implement the Employee interface as shown below.

import net.jini.core.entry.Entry;

public class EmployeeEntry implements Entry{
   public Integer empNo;
   public Serializable empData;
}
public class EmployeeSubClass extends EmployeeEntry
   implements Employee{

   public double calculateBonus(){
      /* impl */
   }
}

This approach gets around the problem of having to provide a default implementation for calculateBonus(). However, it doesn't avoid the problem of exceptions occurring, since a subclass of EmployeeEntry, which doesn't implement Employee, could end up in the space. For a more detailed discussion on this and other entry design decisions please refer back to Chapter 2.

After that brief diversion, let's get back to the plot. To recap, here's the version of EmployeeEntry we are going to use.

public class EmployeeEntry implements Entry, Employee{
   public Integer empNo;
   public Serializable empData;
   //accessors
   public double calculateBonus(){
     throw new UnsupportedOperationException();
   }
}

Having decided on our Entry design strategy and defined the EmployeeEntry, we can now subclass it and provide the actual behavior for two grades of employee. This is shown in the UML class diagram in Figure 8.2.

Figure 8.2. Employee class hierarchy

To keep the example simple, both employee types simply output their class name. Later, when you run this example, the output will be useful to validate that the classes have indeed been downloaded and executed on the client.

public class DirectorEntry extends EmployeeEntry{
   public double calculateBonus(){
      System.out.println(getClass()+" calc");
      double bonus=0.0;
      //code to calculate the bonus
      return bonus;
   }
}

   public class SalesEntry extends EmployeeEntry{
      public double calculateBonus(){
        System.out.println(getClass()+" calc");
        double bonus=0.0;
        //code to calculate the bonus
        return bonus;
   }
}

We now have a starting point for experimenting with mobile code, but before we can complete the example we need to consider how to manage the entries in the space. That is, do we use channels, one for pending and one for completed calculations, a distributed array, or a ticket dispenser? However, we will leave this exercise for you to consider, while we concentrate on discussing code mobility.

To keep the example simple, we will assume that there is only one client, that the empNo field starts at zero, and that all records are consecutive and therefore become exhausted if an entry isn't found for a number in the sequence. This is a dangerous assumption to make, and would lead to all kinds of problems in a real system. However, it will suffice for the purposes of this example.

Employee client code

Below is the outline for the employee client code. In this version, the employee number is incremented from zero until no entry can be taken from the space within a predefined timeout period. We must emphasize that this is not a good strategy, but is used only to keep the example simple.

import net.jini.core.lease.Lease;
import net.jini.space.JavaSpace;

public class EmployeeClient{

   public static void main(String [] args){

       try{
          //get a ref to the space
          JavaSpace space=SpaceLocator.getSpace();

          boolean done=false;
          long timeout=60000L;//wait for 1 minute

          //create a template
          EmployeeEntry template =new EmployeeEntry();

          for(int empNo=0;!done;empNo++){
              template.empNo=new Integer(empNo);
              Employee emp=(Employee)
                  space.read(template,null,timeout);

              if(emp!=null){
                  System.out.println("Got empNo "+
                         ((EmployeeEntry)emp).empNo);
                  emp.calculateBonus();
              }else{
                  done=true;
              }
          }

    }catch(Exception ex){
      ex.printStackTrace();
    }
  }
}

The most important point to note in the client code is that we are using an EmployeeEntry as a template and assuming that the client doesn't have access to any of its subclasses, such as DirectorEntry. By using an EmployeeEntry as a template, the space will match on an EmployeeEntry subclass and return it to the client. As the client doesn't have access to the class files for the EmployeeEntry subclasses, the RMIClassLoader will step in and automatically download the necessary class or classes.

Employee writer code

Before we can run the client we obviously need to get some employees' details into the space. As with the client code, we have tried to keep it as simple as possible so we make the assumption that the lease we request is granted by the space. We have also added one additional helper method which checks that the codebase has been set. It doesn't check that it is a valid codebase, but this should be sufficient to alert you if you forget to set the codebase as a command line argument – see the section on Running the example below.

import net.jini.core.lease.Lease;
import net.jini.space.JavaSpace;

public class EmployeeWriter{

   public static void main(String [] args){

     try{
       //check that the codebase has been set
       checkCodebase();

       //get a ref to the space
       JavaSpace space = SpaceLocator.getSpace();

       long leaseTime=60000L*60L;//lease for 1 hour

       //create some employees
       DirectorEntry director =new DirectorEntry();
       SalesEntry salesPerson =new SalesEntry();

       director.empNo=new Integer(0);
       salesPerson.empNo=new Integer(1);

       //assume leaseTime granted
       space.write(director,null,leaseTime);
       space.write(salesPerson,null,leaseTime);

       System.exit(0);

     }catch(Exception ex){
       ex.printStackTrace();
     }
  }
  private static void checkCodebase(){
      String codebase
         =System.getProperty("java.rmi.server.codebase");

      if(codebase==null){
          String msg="Codebase not set. Use"+
              "-Djava.rmi.server.codebase=";

          throw new RuntimeException(msg);
      }
    }
}

Running the example

To realize the benefit of code mobility, we need to separate the compiled class files into three JAR files (yes, three!): one for the EmployeeClient, the second for the EmployeeWriter, and the third, which contains only the entry classes. The third JAR is the one that clients will download automatically when they read or take DirectorEntry or SalesEntry objects from the space, based on an EmployeeEntry template.

It is worth separating out classes that you expect clients to download from those used solely to process those classes. In this example it isn't critical; however, if EmployeeWriter used lots of other classes and included them in a single JAR file with the entry classes, it would force clients to download the additional classes too.

So far, in this book we haven't listed how to run the examples, since the source and JAR files are available from the book's website. However, we think that it is worth doing here, because this may be a new concept to quite a few readers.

You will also need to download jsip.jar from the website as it contains the SpaceLocator used throughout this book.

Step 1

Create a JAR file for the EmployeeClient, which contains

EmployeeClient.class
Employee.class
EmployeeEntry.class

jar -cf empc.jar Employee.class \
EmployeeEntry.class EmployeeClient.class

This JAR file needs to be appended to either the classpath or environment used to hold the Jini classes and the utility classes from this book, in order to run the EmployeeClient.

Step 2

Next we need to create a JAR file for the EmployeeWriter , which only contains the EmployeeWriter class. This isn't strictly necessary in this case, but it is worth getting into the habit of doing this.

jar -cf empw.jar EmployeeWriter.class
Step 3

Next we need to create a JAR file that contains EmployeeEntry and subclasses. This JAR will be the one referenced by the codebase property set when the EmployeeWriter is run, and the JAR that ultimately gets downloaded by EmployeeClient.

jar -cf emp-dl.jar Employee.class \
EmployeeEntry.class DirectorEntry.class SalesEntry.class
Step 4

Now that we have all our JAR files, we need to set up a class server or web server to point to the location of the downloadable JAR file (emp-dl.jar). Refer to Chapter 6 for hints and tips on how to do this, but let's assume we have configured the alias "employee" to point to the directory containing the downloadable JAR file on the host SG1. We can then specify the codebase as follows:

-Djava.rmi.server.codebase=http://sg1/employee/emp-dl.jar

Finally, we need to compose a script to run the EmployeeWriter. The script will need to have the Jini classes and utility classes in jsip.jar, plus empw. jar and emp-dl.jar must be in either the classpath or set in an environment variable and passed to the Java executable. For example, if we set an environment variable called EMP_CLASSES on Windows that contains the classpath for the above JARs we could launch the EmployeeWriter as follows:

java –cp %EMP_CLASSES% \
-Djava.rmi.server.codebase=http://sg1/employee/emp-dl.jar \
EmployeeWriter.

Run the EmployeeWriter script, followed by the EmployeeClient script. The client code should then output the following in the client, where the two EmployeeEntry subclasses have been automatically downloaded.

Got empNo 0
DirectorEntry calc
Got empNo 1
SalesEntry calc

It may seem like a lot of effort just to see four lines of text output to the console, but we hope it has whetted your appetite for the potential of mobile code and how it can be used with JavaSpaces technology.

Before moving on, it is worth confirming that the EmployeeClient has in fact downloaded emp-dl.jar and not picked up the classes from the classpath. You can probably check your web server log file or, if you are using the web server that ships with Jini, then the console should output the names and codebase of JAR files that have been downloaded.

Alternatively you can use the –verbose flag with the Java executable, which will output details of where classes are loaded from.

Design issues

There are two design issues with this example that you might like to consider as a background task. One issue becomes apparent when a bonus calculation is modified. Under these circumstances an external process would have to take all the entries for that grade from the space, and then write them back with their new class – what happens if this occurs while the bonuses are being calculated, and how can you avoid it happening?

A second point to consider is what happens with the values that result from the bonus calculations. Should they be assigned to a field in EmployeeEntry or written back to the space as a different entry?

Previously loaded classes

Still using the employee example, consider a case where clients are running 24 x 7, performing various tasks on the employee entries such as timesheet analysis, requests for vacations and, once a year, calculating annual bonuses. Far fetched as this example may be, it illustrates a serious point, as a potential bug is lurking when any of the EmployeeEntry subclasses have been updated with new code while the clients are still running and are maintaining a reference to one of the entries.

We highlighted this earlier because there is also a potential problem when the JVM has already seen a class and a more recent one becomes available. The problem occurs because once a class has been loaded by the JVM it will not attempt to reload it unless the class has been garbage-collected. It can only be garbage-collected when there are no reachable instances of that class. Under normal circumstances, this optimization is a good one, but when we are dealing with mobile code and want to keep clients running it becomes an issue that we need to find a workaround for.

There is also the potential for ClassCastExceptions if the serial VersionUID has not been hardwired into the entry that has changed – different versions of the same class are compatible only if they have the same serialVersionUID. See Chapter 3 for more details.

There are two techniques available to you to allow your clients to keep running and be updated after modifying the behavior of your entries: 1 subclassing, or 2 using classloaders.


Using subclassing to modify behavior at runtime

By far the simplest technique to implement with JavaSpaces is to introduce additional subclasses with the new behavior. For example, we could introduce a SaleEntry2 class if calculations for the salesforces' bonuses have changed. This works because the clients are only concerned with EmployeeEntry, so the new class SaleEntry2 would automatically be downloaded. The downside to this approach is that a proliferation of unused classes may occur in the client's JVM over a period of time. This could happen for two reasons.

  1. There are many modifications and therefore new subclasses added to the system.

  2. Class unloading is not guaranteed, or may be switched off/not implemented by the JVM you are using.

We are going to try out this technique with the employee system but before we do so, we should consider which class should be subclassed – EmployeeEntry or SalesEntry?

In cases where you are providing additional processing to the result of the existing class (SalesEntry ) then subclassing it may make sense. e.g.:

public class SaleEntry2 extends SalesEntry{
   public double calculateBonus(){
      double res=super.calculateBonus();
      //apply additional processing to old value
      return res;
   }
}

On the other hand, if the changes to the behavior are bug fixes or performance enhancements, then creating a subclass at the same level as the existing one would make more sense, e.g.:

public class SaleEntry2 extends EmployeeEntry{
   public double calculateBonus(){

      System.out.println(getClass()+" new calc");
      double bonus=0.0;
      //code to calculate the bonus
      return bonus;
   }
}

How about using delegation?

Unfortunately, what would normally be our preferred option isn't really appropriate here due to the matching semantics of JavaSpaces. That is, we would end up with duplicates of all the key fields in EmployeeEntry. This is because we would have to subclass it too. To make matters worse, the delegate would have to be a key field, otherwise it wouldn't get serialized. For more information, see Chapter 2.

For example:

public class SaleEntry2 extends EmployeeEntry{

   //all EmployeeEntry fields are inherited
   //plus we have to duplicate them here
   public SalesEntry impl;

   public double calculateBonus(){
     double res=impl.calculateBonus();

     //apply additional processing to old value
     return res;
  }
}

Modifying the employee system using subclassing

Figure 8.3 shows the interaction with the space when we introduce a new subclass.

Figure 8.3. Downloading new subclasses

Below are the modifications to the employee system that you need to make to see dynamic behavior updates at runtime. If you prefer, you can download the source code from the book's website.

First, you need to include the following class in your project and include the class file in emp-dl.jar:

public class SaleEntry2 extends EmployeeEntry{
   public double calculateBonus(){

      System.out.println(getClass()+" new calc");
      return 0.0;
   }
}

Next, add the lines shown in bold to the EmployeeClient.

public class EmployeeClient{
   public static void main(String [] args){

      try{
         //as before

         for(int empNo=0;!done;empNo++){
            //as before but uses take instead of read
         }
         //v2
         template.empNo=new Integer(1);
         Employee emp=
             (Employee)space.read(template,null,20000L);

         if(emp!=null){
             System.out.println("Got employee no
               "+((EmployeeEntry)emp).empNo);
             emp.calculateBonus();
         }else{
             System.out.println("New Employee not found");
         }
      }catch(Exception ex){
        ex.printStackTrace();
      }
   }
}

Finally, add the lines shown in bold to the EmployeeWriter:

public class EmployeeWriter{

  public static void main(String [] args){

      try{
         //as before

         space.write(salesPerson,null,leaseTime);
         //this is v2
         EmployeeEntry template=new EmployeeEntry();
         template.empNo=new Integer(1);
         //wait for the empNo 1 to be taken, then write in
         //the new version

         while(space.read(template,null,0l)!=null){
             Thread.sleep(1000);
             System.out.println("Waiting ...");
         }

         SaleEntry2 s2e=new SaleEntry2();
         s2e.empNo=new Integer(1);
         space.write(s2e,null,leaseTime);
         System.exit(0);

      }catch(Exception ex){
        ex.printStackTrace();
      }
   }
}

Looking at the code, you will see that initially the client takes EmployeeEntry with empNo equal to #0 then #1. It then attempts to reread empNo #1. Meanwhile, the EmployeeWriter is waiting for empNo #1 to be taken from the space before writing the new implementation class SaleEntry2. Once the new SaleEntry2 has been written the EmployeeClient will read it and display the following text:

SaleEntry2.class calculateBonus()

In the last example, we saw how it is possible, by using subclassing, to make it appear that the behavior associated with a state on an entry has been updated at runtime. Next, we'll look at a different way of doing the same thing, but without having to continually use subclassing to work around problems with the JVM not loading classes it already "knows" about.


Using classloaders to modify behavior at runtime

The second technique described in this chapter is slightly more complex and is applicable to systems other than JavaSpaces. This technique can be used in any system which requires the dynamic update of code at runtime. Although more complex, this is much more flexible for systems that require frequent updates to their behavior, since it alleviates the problem of class proliferation that can occur when you use subclassing. This technique requires a shift in thinking away from the type of polymorphism we've already looked at, as the behavior is not tied to the entry objects themselves, but instead is handled in separate helper classes. In a way, you can think of this as a hybrid of state-based and behavior-based idioms. This is because the entries have their own state plus some metadata that describe where the behavior can be downloaded from.

Here's how we can modify the EmployeeEntry class to utilize the second technique.

public final class EmployeeEntry
   implements Entry, Employee {

   public Integer empNo;
   public Serializable empData;
   public String codebase;
   public String implClass;

   public double calculateBonus()
      throws ClassNotFoundException{
      //download and execute implementation
   }
}

Notice how EmployeeEntry still implements the Employee interface, but no longer explicitly throws an exception. The two new fields are the metadata describing the codebase and the name of the actual implementation class (which implements a known interface).

Before looking at how the code is downloaded and executed, let's take a look at the other side – the implementation classes. For this technique to work, we need to define an interface for the client to use. As with the earlier examples, the client gets shipped only with this interface; it doesn't care what the implementation class is called or where it is located.

Below is a new interface and one of the new implementation classes. Figure 8.4 shows an updated UML diagram for the class hierarchy.

Figure 8.4. Revised employee class hierarchy

public interface EmployeeOps {
   public double calculateBonus(Object data);
}

public class SalesOps implements EmployeeOps{
   public double calculateBonus(Object data){
      System.out.println(getClass()+" "+data);
      SalesData sd=(SalesData)data;
      //do the calc based on the SalesData
      return 0.0;
   }
}

As the new implementation classes are no longer entry subclasses they need to be able to access the state of their counterparts. We have chosen to use a generic object (in the form of a Serializable field) to hold this state, for two reasons:

  1. Each employee grade may have data that is specific to that grade.

  2. We don't intend to pattern-match on this field – it's the payload. In fact, if JavaSpaces supported private fields, this one would be private.

Although using a Serializable field gives us great flexibility, it isn't type-safe, so there is always the possibility (through sloppy coding) of getting a ClassCast Exception (if the wrong object is assigned to this field) when it is cast back to the expected type in the calculateBonus() implementation method.

With the basic framework in place, we can finally get on to the exciting stuff – or scary stuff, depending on your point of view!

You will probably be surprised by how simple it is to download and update your own byte code. So let's waste no more time; let's get stuck in and look at the new implementation for calculateBonus() in EmployeeEntry.

public double calculateBonus()
   throws ClassNotFoundException{

   Class c=RMIClassLoader.loadClass(codebase,implClass);

   EmployeeOps impl=(EmployeeOps)c.newInstance();

   return impl.calculateBonusImpl(empData);
}

Stepping through the new code, line by line, you can see that we use the RMIClassLoader to load the class by passing it the key fields codebase and implclass . Next we use newInstance() on the Class object – for this code to work, the implementation class must have a public no-args constructor.

Finally, we delegate the call to the implementation object impl, passing in the object stored in the key field empData in EmployeeEntry.

That wasn't too painful, was it?

Figure 8.5 shows the interaction that we have just described.

Figure 8.5. Explicitly downloading classes

As it stands, calculateBonus() in EmployeeEntry attempts to download and load the implementation class each time it is called. This is fine because once the RMIClassLoader has downloaded a JAR it will not attempt to do so again (unless the garbage collector has kicked in), even if the timestamp on the JAR has changed. As you don't know when or if the garbage collector will unload the class, it is best to change the name of your JAR file when you provide a code update, to guarantee that the new code is used.

One advantage of this technique over the previous one (that used subclassing) is it can help avoid class proliferation if the JVM you are using has class garbage collection enabled. The reason class garbage collection can kick in with this technique is because of the scoping of the method, meaning that you cannot inadvertently keep hold of a reference to the implementation class; whereas in the subclassing version this is possible.

You can see this in action for yourself by adding the following switch to the Java command line:

-verbose:gc

The code above is the boilerplate for any system that needs to download and update its implementation at runtime. However, if you are interested in using this technique in other systems you might want to consider using a remote properties file that contains the JAR name and name of the implementation class. You can then cache the timestamp on the properties file and an instance of the implementation class (remember that if there is a reachable instance of a class, the class cannot be unloaded). That way, you can compare the timestamps before downloading the JAR, in the same way most web browsers do with images, etc. Also, by using a properties file you can change the name of the implementation class and/or JAR file at any point.

Before leaving the employee system example, it is worth thinking of differences between the design of the subclassing and classloading examples. In the last example we separated state and behavior into two groups of classes, where EmployeeEntry contained the state and a hierarchy of EmployeeOps classes implemented the behavior – so is this a good thing to do in the context of JavaSpaces?

Well, it depends on your point of view. One point of view is that entries are more like "records" than true classes, because you can't encapsulate both (private) state and behavior in the traditional sense – fields must be public, and private state is not preserved. This school of thought continues by stating that entries should be used for pattern matching and the manipulation of an entry's data should be handled by other classes.

The other school of thought is that you should make entry classes as "true" as possible by providing accessor and behavioral methods as we examined in Chapter 2. Most examples you will see in the JavaSpaces community tend to fall partially into this category, some state and some behavior.

We are sure this debate will run and run…


Security and mobile code

As you have seen, the ability to download and execute methods on classes that your client doesn't know about is extremely powerful. However, this comes at a price – the risk that the unknown code might attempt to do something to compromise your system.

Fortunately, Java's security mechanism comes to the rescue with its security managers, policy files, and JAR signing, all of which can be used to restrict what downloaded code can do.

The whole topic of security and how to use it in Java, let alone with Jini, is vast and beyond the scope of this book; however, we wanted to bring this issue to your attention and provide a few pointers to get you thinking about it.

If you are using one of the techniques described in this chapter in a JavaSpaces environment, then securing/restricting access to the space to known clients is a good starting point in order to stop malicious code being introduced into the system. A second option is to create a very restricted policy file that only grants "risky" operations to code loaded from the classpath. The problem with this approach is that some of your downloaded implementation classes may need access to sockets or the file system, etc.

A third option is to only grant "risky" permissions to signed JARs from a known "signer". This is a good choice, but requires far more configuration during development and at deployment time.


Summary

In this chapter we have seen how Java's built-in code mobility is used behind the scenes when you interact with a Jini service such as a JavaSpace, and how MarshalledObjects can be used to transport objects between JVMs without all the dependent classes for the "real" serialized object being downloaded (when the MarshalledObjects are deserialized) in the recipient's JVM.

We have also seen how code mobility comes into play when subclasses of entries unknown to a client are read or taken from the space, and how the subclassing technique can be used to add or change behavior in a running system.

Finally, we examined how separating an entry into two classes (one holding the matchable state and some metadata, and the other the behavior) meant that we could use the RMIClassLoader to download the version of the implementation we require at runtime.


Chapter 9. Using transient and persistent spaces

In the introduction to this book, we mentioned that JavaSpaces come in two basic flavors – transient and persistent. When JavaSpaces services are deployed they are either deployed with a transient or persistent storage model that exists for the lifetime of the space. We don't know of any space that can switch its storage model while it is running, or indeed why this would be useful.

This makes the selection of type of storage model for the space an architectural issue in most cases. So, by way of an introduction, we are going to characterize the properties of both types of space so that systems specifiers and designers can make an informed decision early in the system design cycle.

It is important to remind ourselves that the JavaSpaces interface is entirely neutral to the storage model being used by the underlying spaces implementation, so when we see code that refers to a space that is performing Space API operations, the type of storage model will not be visible at this level. It is assumed that the writer and reader of the Java code know the type of storage implementation.

After characterizing the storage models, we are going to show how clients can selectively access transient or persistent spaces as they are running, and then we will look at systems that can select (at the class and object level) the type of persistence required, thus adding more and more flexibility into the selection of storage models.


Transient spaces

In the case of transient spaces the entries are stored in volatile memory. In essence, this means that if the space crashed then users would lose all of the information contained in entries stored in the space. Another way to think about this is that all of the leases on the entries in the space are expired if the containing space is shut down or crashes.

It is thus easy to think that transient spaces don't need to store any information that can tolerate a power-down or reboot; but this is not the case. Because a transient space has to deal correctly with transactions, it must record a crash count each time the system is stopped or rebooted. The crash count is described as part of the two-phase commit protocol in the Jini specification. If you want more detail on this mechanism, it is best to check the Jini specification, but for our purposes it is only important to understand that the transient space can communicate with the transaction manager; meaning that the transaction model can ensure its ACID (Atomicity, Consistency, Isolation and Durability) properties.

In general, systems that use transient spaces can outperform those using persistent spaces because entries that are written to them do not need to also be moved on to the persistent store. This can make a significant difference to the overall throughput of a space-based system.

Many first-time designers of space-based systems tend to err on the side of caution and specify the use of persistent spaces in case of system failure. However, for many applications, transient spaces provide enough guarantees via the transaction mechanism to make them very useful, and fast, components of a distributed system.

Generally, in cases where high throughput and short object lifetimes are required, a transient space is a much better fit for the implementation. For example, in patterns that use the space as an "object message bus", transient spaces are ideal.

As an extreme example, if the specified lease times of the objects in the space are less than the specified reboot time of the host space, then the only sensible choice is a transient space.


Persistent spaces

With persistent spaces, the state of almost all the space is stored in non-volatile memory, perhaps a RAID (Redundant Array of Inexpensive Disks/Devices) disk array for example, which means that the space can tolerate crashes in the system hosting the space's server. Figure 9.1 shows a high-level view of a persistent space, which includes an implementation in the persistent support section of the architecture.

Figure 9.1. Persistent space

As lease times become longer and the information that is contained in the space becomes more valuable, the case for persistent spaces becomes much greater. Again, using transactions offers the ACID guarantees at a system level. Of course, persistent spaces are slower and hence offer less throughput than transient spaces, but may offer the ability to store many more objects than transient spaces. This is because persistent space does not need to store all of the entries and associated objects in volatile memory as the space is running. By using memory paging – or the object/space-based equivalent – a smart space server can implement vast storage areas, moving entries in and out of local memory as required.


Accessing different spaces

Making a design decision on which type of space to use for a new system isn't always as simple as making a straight choice between using either a transient or persistent space, as your analysis may identify the need for both transient and persistent entries. In this case, the simplest and most obvious approach is to use proxies for both types of space, and use them selectively as required. Assuming some obvious naming conventions, here is the code.

JavaSpace persistent = SpaceLocator.getSpace("persistent");
JavaSpace transient = SpaceLocator.getSpace("transient");

We then simply use the appropriate JavaSpace reference to perform the writes, reads, takes, etc. This tends to work well if our analysis model for each of the entries was good. However in cases where, for example, an entry slowly becomes more important to us and we are not concerned about performance, we may want to respecify its storage into a persistent space. Of course, switching the storage model for an entry to a transient space is also viable. In these cases, we can construct a more flexible system by specifying the storage model as part of the entry.


Specifying storage models with entries

In this section, we are going to develop an adapter proxy to support a mixed storage model JavaSpace. Adapter, wrapper, and smart proxies are Jini proxies that delegate to an underlying "real" proxy. In this case, the adapter proxy will implement the same interface as the "real" proxy – the JavaSpace interface.

Adapter proxies are useful for many types of Jini services, for example, by intercepting calls to the real service, an adapter can record logging information, both before and after delegating the call to the real service. We look at how to apply that technique to a JavaSpace in Chapter 13.

In this instance, we are going to develop an adapter proxy that will load a proxy to both a transient and a persistent space, and forward the API calls to the appropriate space – we call this a mixed proxy, and it is shown in Figure 9.2.

Figure 9.2. Mixed proxy

As we want to take advantage of specifying the storage model as part of the entry, we have decided to start by defining two marker interfaces. We can then use these to "mark" the entries that we want to store.

public interface PersistentEntry
      extends net.jini.core.entry.Entry { }

public interface TransientEntry
         extends net.jini.core.entry.Entry { }

When the mixed proxy is loaded into a client, it will dynamically load the proxies to both the transient and persistent space and embed them so that calls to either can be made (Figure 9.2).

As our MixedProxy interface is going to extend the JavaSpaces interface, we know that all the entries that will arrive at this interface are either going to be marked TransientEntry, PersistentEntry, or simply Entry. We can't stop people just using vanilla entries if we want to retain complete compatibility with the JavaSpaces interface.

To simplify matters, we have to decide on a default JavaSpace where all the vanilla entries can be put. In our example, we have chosen the persistent space, but we could just as easily have used the transient space.

We now have a very simple set of rules for forwarding the entry to the appropriate space. We can state that if the entry is marked with the TransientEntry marker, then we forward it to the transient space, and if the entry is marked as PersistentEntry or simply Entry then we put it in the default space, which in this case is also the persistent space. Let's take a look at the MixedProxy class.

public class MixedProxy
    implements JavaSpace, Serializable{

    private JavaSpace _defaultSpace;
    private JavaSpace _specialSpace;

    //has to correspond the special case below
    private Class specialClass
                 = TransientEntry.class;

    private void findSpaces() {
      try {
        _defaultSpace=
            SpaceLocator.getSpace("persistent");
        _specialSpace=
            SpaceLocator.getSpace("transient");
        }
        catch (Exception exp) { exp.printStackTrace(); }
      }

    private JavaSpace getSpaceForEntry (Entry entry){

        if( _specialSpace==null ||
          _defaultSpace==null){
        findSpaces();
    }
    if(specialClass.isAssignableFrom(
                      entry.getClass())){
      return _specialSpace;
    }
    else {
        return _defaultSpace;
    }
  }
  public Lease write(Entry entry,
            Transaction txn,long lease)
    throws net.jini.core.transaction.TransactionException,
           java.rmi.RemoteException {
      JavaSpace space=getSpaceForEntry(entry);
      return space.write(entry,txn,lease);
  }

  // … other JavaSpaces methods …

  public Entry snapshot(Entry entry)
     throws java.rmi.RemoteException{
      return entry;
  }
}

Looking at the above code for the MixedProxy class, you can see that for every call we make to the JavaSpaces methods we first make an internal call to the getSpaceForEntry() method, which checks for the "special" marker interface and returns the appropriate proxy for that marker. The method returns a reference to the proxy rather than assigning it to a field in the proxy. This is to avoid race conditions if the client is accessing the proxy concurrently.

The only method that follows a different pattern is the snapshot method, which simply returns the entry, short-circuiting calls to the proxies. The reason for this is that if we forwarded the call to the space proxy we may receive an entry back, no longer marked by the PersistentEntry or TransientEntry interface. This is because the snapshot method is only obliged to return an object of type Entry: how the object is structured internally or the actual or concrete type of entry should be ignored by the space client. So, in order to be sure that the marker interface is maintained, we have to maintain, the object in its original form.

The problem with our simple implementation of snapshot() in the MixedProxy class is that we are taking away a good optimization from the client – so is there anything we can do to improve matters?


Mixing snapshots

To improve on our first simple snapshot implementation, we need to create a map that relates the entry returned from snapshot() to the space that created it. We store this information in a hash table called _snapshots, as you can see from the code below. In this way we can check the hash table in the getSpaceForEntry(), method and select the correct space for the snapshot entry.

Generally, people rarely create larger numbers of snapshots, but for this technique to work over a long period of time we will need to use a java.util. WeakHashMap, otherwise we could run out of memory as the hash table grows. To implement this technique, the first thing we need to do is to add a new field to MixedProxy – you will see when this is initialized later on.

private WeakHashMap _snapshots;

We can now modify snapshot() to use the snapshot entry as the key and the originating space as the value, thus creating a mapping between the snapshot entry and the space that created it.

public Entry snapshot(Entry entry)
        throws java.rmi.RemoteException{

   JavaSpace space=getSpaceForEntry(entry);
   Entry snapshot=space.snapshot(entry);
   _snapshots.put(snapshot,space);
   return snapshot;
}

Finally we need to modify the getSpaceForEntry() method to perform a lookup on the entry parameter in the _snapshots object. If it returns a non-null value then that's the space we should be using. Checking the hash table for the existence of the entry parameter lets us know whether or not the entry is in fact a snapshot. The code below shows the additional condition check that needs to be added to getSpaceForEntry().

private JavaSpace getSpaceForEntry(Entry entry) {

  if(_specialSpace == null
         || _defaultSpace == null){

    findSpaces();
    //create WeakHashMap
    _snapshots=new WeakHashMap();
  }else{
    //this is only valid if we already have
    //references to the space
    JavaSpace space=
         (JavaSpace)_snapshots.get(entry);
    if(space!=null){
         return space;
    }
  }
  //same as before
}

That's all there is to it, apart from registering the proxy to the Jini Lookup Service.


Registering the mixed proxy

In order to register the MixedProxy to the Lookup Service, we write a very simple program to "publish" the proxy. One important thing to note in the code for RegisterMixedProxy is that, as the proxy is a simple Serializable object and not an RMI object such as a UnicastRemoteObject, we must have a loop at the end of main to stop the JVM exiting. This is because if the JVM exited, the JoinManager wouldn't be able to renew the proxy's lease in the Lookup Service, resulting in the proxy being removed from the Lookup Service when the lease expired.

public class RegisterMixedProxy {

  public static void main(String [] args){
     try
     // set the rmi security manager
     if (System.getSecurityManager() == null) {
         System.setSecurityManager(new RMISecurityManager());
         }

         // create a proxy to put into Reggie
         MixedProxy proxy = new MixedProxy();

         // join callbacks, ignoring persistent
                                         service id for now.
         ServiceIDListener listener=new ServiceIDListener(){
           public void serviceIDNotify(ServiceID serviceID) {

           // not storing for now
           Debug.message("serviceIDNotify()");
           }
       };

     Entry [] attribs=new Entry[]{
           new Name("Mixed Space")
     };
       _joinManager=new
          JoinManager(proxy,attribs,listener,null,null);

       // allow the join manager renew the lease
       while(true){ Thread.sleep(10000); }

     }catch(Exception ex){
         ex.printStackTrace();
    }
  }

  private static JoinManager _joinManager;
}

A second point to note in the RegisterMixedProxy example is that we are not storing and reusing the ServiceID provided by the Lookup Service. This is to keep the example simple. However, storing and reusing ServiceIDs is some-thing you should always do. The simplest way to do this is to serialize ServiceIDs to the local file system; that way, each time you want to register a proxy the code can check for the existence of the file and use the stored ServiceID if one exists.

Failure to reuse ServiceIDs results in "dead" proxies residing in the Lookup Service until their leases expire. In contrast, by reusing a ServiceID the Lookup Service will replace the matching proxy with the new one.


Summary

JavaSpaces come in two flavors: transient and persistent. With transient spaces, the entries are stored in volatile memory. This means that if the space crashes, then users will lose all the information contained in entries stored in the space.

In general, systems using transient spaces can outperform those using persistent spaces because entries that are written to them do not need to also be moved on to the persistent store. This can make a significant difference to the overall throughput of a space-based system.

In persistent spaces, the state of almost all of the space is stored in non-volatile memory, perhaps a RAID disk array for example, which means that the space can tolerate crashes in the system hosting the space server.

By defining a marker interface, it is possible to implement a mixed proxy that can direct entries marked as transient to a transient space and those marked as persistent to a persistent space.

It is also possible to control the selection of transience and persistence by examining an entry's lease times. To implement this technique, you could define a threshold lease time for which entries below that threshold get routed to a transient space and others are routed to a persistent space. This would allow you to use a transient space for short-lived entries, thus improving performance, while maintaining longer-lived entries in the persistent space. For this technique to work with remote events, you will have to write a RemoteEvent Listener wrapper class that can register with both spaces (from within the proxy), thus maintaining correctness with the client.


Part III: Application

    Chapter 10. Process coordination

    Chapter 11. Location-based services

    Chapter 12. Bidding and agent systems

    Chapter 13. Testing space-based systems

    Chapter 14. Tuning space-based systems


Chapter 10. Process coordination

Coordinating threads in a single process can be complex to get right, because we need to make sure that we avoid race conditions and deadlocks. Debugging and identifying problems like these in a single process can be time-consuming, as race conditions and deadlocks may not occur consistently or may vary depending on the processor speed, platform, or whether the machine the process is running on has multiple processors. Coordinating components in a distributed system is usually even more complex and harder to debug.

Using JavaSpaces can help reduce the level of complexity, through the use of Transactions and the atomic nature of the write/take methods.

This chapter starts by looking at the basic request–response model used in many traditional distributed systems and then sees how using JavaSpaces can help make the model cleaner by decoupling the requester from the responder. We will also examine how using JavaSpaces with this type of model can be more robust in the event of failure.

The example we are going to use throughout this chapter is based around the common business model of processes interacting with a database. We have picked this example as we hope that you will be able to spot the similarities with systems you've developed yourself, and be able to come up with new variations to suit your business/problem domain.


Request–response model

In a standard peer-to-peer (or three-tier) distributed application, a client invokes a method on the remote service. Figure 10.1 shows this in action with a three-tier architecture where the client sends a request to a service that retrieves some data from a database. The service may then manipulate the data before returning the result to the client.

Figure 10.1. Three-tier architecture

In a peer-to-peer model, the client is dependent on a single service to handle requests. If that service crashes, the client must perform another lookup to see if there is another matching service available. However, in practice the middle tier in such architectures does not provide fault tolerance or cope well with partial failure, due to the complexity of sharing data structures and locks.

By using a combination of Jini services and one or more JavaSpaces it is possible to implement a distributed request–response model that is more robust and flexible in the event of partial failure, because the JavaSpace decouples the dependencies common in three-tier architectures.

Figure 10.2 illustrates this model where the space is being used to coordinate the interaction between a client and a (in this case single) service.

Figure 10.2. Coordinating interactions with a JavaSpace

By using the space(s) to coordinate the interaction between the client and the service it allows us to run multiple services to handle the requests and, in the event of failure, to restart one or more of the services while the others continue with their work.

This model also allows for load balancing if the services are run on different hosts. In our example, the database is the obvious bottleneck and single point of failure; however, by using replicated or mirrored databases it is possible to reduce the risk of failure.

You may be wondering why we're not considering the JavaSpace as a single point of failure. This is because we can use a combination of dynamic (smart) Jini proxies and multiple JavaSpaces. A dynamic or smart Jini proxy can be one that performs lookup discovery itself (on the client) and then uses delega-tion to forward requests to the discovered proxy. This type of proxy can then switch services at runtime if it detects a problem. We use a similar technique in Chapter 13.

Figure 10.3 shows how the client interacts with the Jini lookup service and the dynamic proxy.

Figure 10.3. Dynamic proxy

This type of design isn't suitable for every system, but for systems the entries of which have short lease times switching spaces at runtime, if one becomes unavailable, it is a viable option. For systems that require a persistent space, some vendors now provide clustered and replicated JavaSpaces so the space doesn't become a single point of failure.

After that brief diversion into the world of dynamic proxies, let's return to the request–response model as a whole and look at some enhancements.

Figure 10.4 shows the original model, but with an additional service that uses a replicated (or mirrored) database. To keep the diagram simple, we have only shown one additional service; however, bear in mind that this model is very scaleable because you can have as many services as required. As a rule of thumb, models like this are most effective when there is more than one service available to handle requests, thus allowing for the load balancing we mentioned earlier and fault tolerance when one of the services becomes unavailable.

Figure 10.4. Using multiple services

For this model to be robust you may think that the client program needs to be designed so that it can detect if a request hasn't been serviced. This could happen for a number of different reasons; for example, if the service that took the request crashed before it wrote a response back to the space. Fortunately, implementing this kind of strategy is surprisingly easy when working with JavaSpaces, so long as we make sure that the service which takes the request does so in a transaction. That way, if the service crashes between taking the request and writing the response the space will take care of restoring the original request entry when the transaction expires, allowing one of the other services to handle the request.


Implementing the model

To implement a space-based request–response model we need two entries, one to represent the request and the other for the response. To keep this example simple (as always), we are simply going to embed some SQL into the request entry, and place the resulting data and column names in the response entry.

Here's how the request entry looks.

public class SQLRequest
    implements Entry{

    public Long _requestId;
    public String _databaseName;
    public String _sql;
}

The request entry has three fields. The first field will contain the unique ID of the request, which we will need later on to match on the response entry – we discussed how to generate unique IDs in Chapter 5, so we won't go into that here.

The second field is to identify the database and the third is to hold the SQL statement we want one of the services to execute on our behalf. The second part of the story is the response entry, which we define as follows.

public class SQLResponse
    implements Entry{

    public Long _requestId;
    public String [] _colNames;
    public Object [][] _data;
}

The response entry contains the unique request ID (so that the originator can match on it) and two fields that hold the column names and data returned by the query.

We are not going to go into detail here about how the service is implemented, since we want to focus on the usage of the client. However, what you do need to know is that when the service has handled the request it writes an SQLRepsonse entry to the space containing the results and the unique ID of the SQLRequest.

Now that we have the entries we need to interact with the space, let's look at an example of some client code.

JavaSpace space=SpaceLocator.getSpace();

Long reqId=getUID();
SQLRequest req=new SQLRequest();
req._requestId=reqId;
req._databaseName="STARGATE:SG1";
req._sql="select * from episode where series=6";

//life time of the request
long leaseTime=FIVE_MINUTES;
space.write(req,null,leaseTime);

//create a SQLResponse template
SQLResponse tmpl=new SQLResponse();
tmpl._requestId=reqId;

//how long we're prepared to wait for a response
long waitTime=TEN_MINUTES;

//now wait for the response
SQLResponse resp=(SQLResponse)
      space.take(tmpl,null,waitTime);

if(resp==null){
    //take some action
    //maybe all services are busy
}else{
    //process response
}

If you're not familiar with Jini, it's worth pointing out that the concepts of "client" and "server" blur very quickly, as services are frequently clients of other services. So when we refer to a "client" you should read it as interchangeable with "service".

In the current context the "client" is most likely to be another Jini service that uses the other services to retrieve the data it requires from the database to perform its task. For example, a risk engine could be both a client of the SQL service and also a service in its own right; however, by using the space-based request–response model as a mediator between services it can still react to direct requests, but also collaborate in a decoupled model with other services.


Coordinating multiple services

In the previous section, we examined a simple two-way communication protocol through the use of a space-based request–response model. In this section we are going to build on that model to allow more than one service type to handle the request via a JavaSpace. The difference in this model is that each service performs some task based on the results of a task performed previously by another service before returning the final response to the space.

The underlying motivation for this design is to decompose a large system (or single service) into several smaller more manageable and reusable service components. Figure 10.5 shows this in action.

Figure 10.5. Multiple interactions

The three services shown in Figure 10.5 have the following roles.

  1. Request Mapper: This service is responsible for converting the original request parameters into something that the SQL service can understand. This is to remove the client's dependency on SQL. It also embeds a context field in the request that gets passed on through the other services.

  2. SQL Service: Executes a query or stored procedure and writes the results to the space.

  3. XML Formatter: Matches on a context field (originally set by the Request Mapper) in the results from the SQL service, indicating that the results need to be converted into XML. The XML Formatter then performs the conversion and writes the results back to the space. Finally, the client takes the XML response from the space.

By applying the same techniques described for the first example of a space-based request–response model, it is possible to build a highly resilient system the component services of which can also be reused as new systems or requirements come along.


Summary

The traditional request–response model is tightly coupled. If the service handling the requests crashes, you will usually need to restart all the clients as well as the service.

By using a combination of Jini services and one or more JavaSpaces, it is possible to implement a distributed request–response model that is more robust and flexible in the event of partial failure, because the JavaSpace decouples the dependencies common in three-tier architectures.

Using a smart proxy in a client means that it can transparently switch between spaces if one becomes temporarily unavailable. Correct usage of transactions and lease times allows the space or client to recover if something goes wrong in one of the collaborating services.

The model we have looked at in this chapter is one of countless variations of how a JavaSpace can be used; we hope, however, that we have whetted your appetite for the potential usage of JavaSpaces in many different domains and that you're now rushing off to try this out for yourself.


Chapter 11. Location-based services

This chapter looks at location-based services implemented with spaces, and discusses how the use of generic or more abstract classes can help to reduce complexity and increase flexibility and future growth in these and other space-based systems. We will also be taking a look at how the inherent matching properties of spaces can be used to implement powerful searching strategies, and making use of their inherent parallel character to apply optimization to searches.


Location-based systems

One of the much vaunted differences between "traditional" telephone and communications systems and the world of mobile devices is that not only can we communicate with "distant" devices, we can also establish the location of these devices. Using this information, we can use computing devices (not only desktop computers, of course) to implement location-based services.

In knowing the locations of various devices at a given time we can build rich information systems in a way that is simply not possible with fixed-line systems. Of course, this places greater emphasis on the timeliness of systems – it is no good knowing that a truck was in our region two days ago when it is 500 miles away now – and it also implies that systems can be distributed, even on a very large scale.

For example, scheduling and logistics systems can be built with JavaSpaces. Using location-based information, we can make more effective use of transportation facilities, such as trucks, buses, trains, and other modes of transport to carry goods and passengers. Any improvement in our transport networks and infrastructure must be seen as a good thing in times of great stress on the environment.

We believe that the use of JavaSpaces could have a very large impact on such services. In the following case study, we are going to show you some code that we have used in practice to support this argument.


Packit Shifters – case study

For the purposes of our (simplified) example, imagine that we have been asked by a local delivery company called Packit Shifters to help them design and build a system to track the location of their delivery vans. They use the vans to pick up and deliver small packages and special delivery items. The vans operate from a small depot, which is located centrally in the local delivery area.

Packit Shifters currently use mobile phones and local radios to track the location of the vans. A highly skilled scheduler works at a depot to coordinate the collection and delivery of items via the vans. As the number of vans and customers has increased, the scheduler's job is becoming increasingly difficult and certain parcels are being missed because the whole of the scheduling "picture" is being held in the scheduler's head – sometimes she is simply getting overloaded.

At the same time, Packit Shifters also realized that this increase in activity is presenting them with an opportunity to use their resources more effectively. Because they have more vans out on the road, if they could reschedule vans dynamically to cope with new collections and deliveries, they could help more customers in less time.

The company wants to start developing location-based services to help them with this dynamic rerouting and to take some of the stress from the scheduler's job. To start with they installed GPS (Global Positioning System) location devices into the delivery vans and provided a data feedback to the depots so that the position of the vans can be tracked more accurately.

For the sake of simplicity, we have divided up the delivery area into a grid with 100-meter spacing. The company has decided that its area for collections and delivery is 50 kilometers in every direction from the depot, which makes a grid of 100 km by 100 km. (Actually, it makes a circle with a radius of 50 km, but we are using a grid, again to keep things simple.)

So we have a grid that we overlay on to the delivery area, which is 1000 by 1000 locations, and we locate the origin of the coordinates at the bottom left corner (see Figure 11.1).

Figure 11.1. Delivery area © Crown Copyright.NC/A7

We call the vertical location on the grid the latitude and the horizontal location the longitude. By the way, this is not the "real" geocentric latitude and longitude, we just use these names by convention to suggest that the model we present could be scaled to larger domains if we choose to do this.

First, here is some simple code which represents a delivery van in our system.

import net.jini.core.entry.*;

public final class DeliveryVan
  implements Entry {

  public Integer _lat; // latitude in this class
  public Integer _lon; // longitude in this class

  public String _registration; // van reg. number
  public String _driverName;
  public String _mobileNumber;

  //define the null constructor
  public DeliveryVan() { }

  // define the populating constructor
  public DeliveryVan(String registration,
               String driver, String mobile) {
    _registration = registration;
    _driverName = name;
    _mobileNumber = mobile;
  }

  // location accessors
  public void setLocation(int lat,int lon){
    setLatitiude(lat);
    setLongitude(lon);
    }

  public void setLatitude(int lat){
    _lat = new Integer(lat);
  }
  public int getLatitude(){
    return _lat.intValue();
  }

  public void setLongitude(int lon){
    _lon = new Integer(lon);
  }
  public int getLongitude(){
    return _lon.intValue();
  }
  // ... other Van detail accessors ...
}

As you can see, although the fields of the Entry are public we have made accessors (gets and sets) for all the values although we have elided the ones for specific Van details.

Now that we have an Entry that represents the delivery van, the next step is to make one for the Depot.

public final class Depot
  implements Entry{

  public Integer _lat;
  public Integer _lon;

  public String _name;
  public String _dutyManager;
  public String _phoneNumber;

  //define the null constructor
  public Depot() { }

  public Depot(String name,String dutyManager,
                           String phoneNumber){
    _name = name;
    _dutyManager = dutyManager;
    _phoneNumber = phoneNumber;
  }

  // location accessors
  public void setLocation(int lat, int lon){
   setLatitiude(lat);
   setLongitude(lon);
  }

  public void setLatitude(int lat){
    _lat = new Integer(lat);
  }
  public int getLatitude(){
    return _lat.intValue();
  }

  public void setLongitude(int lon){
    _lon = new Integer(lon);
  }
  public int getLongitude(){
    return _lon.intValue();
  }
  // ... other Depot detail accessors ...
}

Already it is becoming obvious that we are repeating all the details that refer to location, and we can also infer that the code that we are going to write to process location-based information is going to be highly repetitive too. Even with just two location-based actors in the space we can see that there is a need to move the location-based information and methods into a more general Locatable class. As the number of location-based actors grows, this refactoring gives us much less complexity to manage, and allows us to expand the system more quickly and with fewer bugs.

With that in mind, here is the code of our new Locatable class.

public abstract class Locatable
  implements Entry{

  public Integer _lat;
  public Integer _lon;

  public Locatable() { }

  public Locatable(int lat,int lon){
    setLocation(lat, lon);
  }

  public void setLocation(int lat,int lon){
    setLatitude(lat);
    setLongitude(lon);
  }

  public void setLatitude(int lat){
    _lat = new Integer(lat);
  }
  public int getLatitude(){
    return _lat.intValue();
  }

  public void setLongitude(int lon){
    _lon = new Integer(lon);
  }
  public int getLongitude(){
    return _lon.intValue();
  }
}

and here is the refactored Depot that extends the Locatable class.

public final class Depot extends Locatable{

  public String _name;
  public String _dutyManager;
  public String _phoneNumber;

  // define the null constructor
  public Depot() { }

  public Depot(String name,String dutyManager,
                           String phoneNumber){
    _name = name;
    _dutyManager = dutyManager;
    _phoneNumber = phoneNumber;
  }
  // ... other Depot detail accessors ...
}

The delivery van refactors in exactly the same way, so we won't repeat the code. As well as the benefits in terms of flexibility and future expansion possibilities, it also allows us to write services that use Java's polymorphic capabilities to solve some location-based problems.


Finding locatables in a space

Now that we have defined some of the basic actors in the system and we have a refactored abstract model for handling the location-based aspects of the actors, we can move on to develop a service that utilizes the actors.

The first service is described by a new class called Locator, which can perform the set of locating tasks we define by acting as a space client and finding the appropriate entries in the space. The two key activities for the actors are for the Depot to find the nearest Van and for the Van drivers to find the Depot within a given space.

public final class Locator{

  private JavaSpace space;

  public Locator(JavaSpace searchSpace){
    space = searchSpace;
  }

  public DeliveryVan findNearestVan(Depot depot){
    //impl
  }

  public Depot findNearestDepot(DeliveryVan van){
    //impl
  }
}

Our job now is to write some clients of the system that help the scheduler to find the relative location of various locatable actors in the system.

The code segment below shows how we envisage the Locator class being used to find the nearest delivery van to the Depot. The name of the Depot is supplied in the form of the string depotName.

JavaSpace space = SpaceLocator.getSpace();

// first get the named depot from the space
Depot template = new Depot(depotName, null, null );
Depot depot =
     (Depot)space.read(template, null, JavaSpace.NO_WAIT);

// attach the locator to the space that we want to search.
Locator locator = new Locator(space);

// find the nearest Van
DeliveryVan van = locator.findNearestVan(depot);

It looks like the Locator's interfaces give us a relatively clean and simple model at the client level, so let's dig deeper into implementing the Locator.

Implementing the Locator

As a starting point for implementing the Locator, let's consider one of the methods in the Locator's interface, the one that we used in the example above – findNearestVan(). Let's look at the code to find out if there is a van in the same square as the Depot:

public DeliveryVan findNearestVan(Depot depot){

  // make a template to find a van.
  DeliveryVan tmpl = new DeliveryVan();

  // set the location of the template.
  tmpl.setLatitude(depot.getLatitude());
  tmpl.setLongitude(depot.getLongitude());

  // try to find it in this grid square.
  //Null means no object found of course.
  return (DeliveryVan)_space.read(tmpl, null, SHORT_TIME);
}

Here we wait for a SHORT_TIME just in case the delivery van has been taken from the space, to update its coordinates, for example. This must be balanced against the need for the method to return within a reasonable time to the client code. In this case there is no perfect length of time, but one should be chosen that balances the need to get the space client "live" while making sure that we don't miss a temporally updated entry. In this case as in many others we are dependent on other space clients not holding on to entries longer than is absolutely necessary as part of their take/write cycle. We look at some tuning techniques related to this in Chapter 14.

So far so good, but if there is no match we will want to start looking outward into adjacent squares to see if there is a van near to this location. Here is the extended implementation, which looks both within the "local" square and into the four adjacent squares.

// make a template to find a van
DeliveryVan tmpl = new DeliveryVan();
DeliveryVan found = null;

// set the location of the template.
tmpl.setLatitude(depot.getLatitude());
tmpl.setLongitude(depot.getLongitude());

// try to find it in the local grid square.
found = (DeliveryVan)_space.read(tmpl, null, SHORT_TIME);
if ( found != null ) return found;

// if we get to here then it's not been found
//so look in adjacent squares

// look north of the depot
tmpl.setLatitude(depot.getLatitude()+1);
tmpl.setLongitude(depot.getLongitude());
found = (DeliveryVan)_space.read(tmpl, null, SHORT_TIME);
if ( found != null ) return found;

// look to the east
tmpl.setLatitude(depot.getLatitude());
tmpl.setLongitude(depot.getLongitude()+1);
found = (DeliveryVan)_space.read(tmpl, null, SHORT_TIME);
if ( found != null ) return found;

// look to the south
tmpl.setLatitude(depot.getLatitude()-1);
tmpl.setLongitude(depot.getLongitude());
found = (DeliveryVan)_space.read(tmpl, null, SHORT_TIME);
if ( found != null ) return found;

// go west young van
tmpl.setLatitude(depot.getLatitude());
tmpl.setLongitude(depot.getLongitude()-1);
found = (DeliveryVan)_space.read(tmpl, null, SHORT_TIME);

// finished the search
return found;

Already, this is becoming repetitive. Please forgive all the code duplication, but the point we are making is that we have very quickly reached the stage where a number of refactorings would be useful, and another interesting option exists. Instead of looking into the adjacent squares sequentially, we could have done this in parallel. If you are interested in how we can use this inherent parallelism to speed up location-based searches, then you might like to skip to the section on optimizing searches. Now, we will look at the possible refactorings that we can make on the Locator.

Refactoring the Locator

The code we have generated so far is serving its purpose, but we have already encountered two design problems. First, it looks like we are going to have to work out a more general way of performing the searching behavior; we can't simply cut and paste the code from one "typed" searching method to another. For example, findNearestDepot() is going to look very similar to findNearestVan() and so we need to refactor the common behavior into a common method.

Luckily, we already have the Locatable generic type defined for our entries so we can make the common searching method work at this level of abstraction. This new method should simply find the nearest Locatable object. Here is the suggested method. For simplicity, we have only looked at finding any "local" entries in the space.

private Locatable findNearest(Locatable origin,
                         Locatable templ){
  tmpl.setLatitude(origin.getLatitude());
  tmpl.setLongitude(origin.getLongitude());

  // try to find it in this grid square
  return (Locatable)_space.read(tmpl, null, SHORT_TIME);
}

Given this new abstracted approach, we can reimplement the findNearestVan() method very easily by delegating the call to findNearest() .

public DeliveryVan findNearestVan(Depot depot){
  DeliveryVan template = new DeliveryVan();
  return (DeliveryVan)findNearest(depot, template);
}

… and findNearestDepot() also becomes trivial …

public Depot findNearestDepot(DeliveryVan van){
  Depot template = new Depot();
  return (Depot)findNearest(van, template);
}

In both cases (as in any further extensions we could now make), we are using the underlying type-matching semantics of the space to make sure that Vans only match Vans and Depots only match Depots, etc. So we have a neater and more simply extensible design for our Locator. However, we are falling short of the stated goal of the findNearest() method. This method only looks in the current grid square, and the stated goal is to find a Locatable entry that is of the same type as the template, and which is nearest to the "origin", if indeed any matching entries exist in the space.

The problem of implementing findNearest() now becomes one of implementing a method that can efficiently search not just the local square but also adjacent squares. We started down this track in the previous section by implementing some code in our repetitive findNearestVan() method.

One way to solve this type of problem is as follows: starting from the origin object, we send out an ever-expanding circle over the search grid, much like dropping a pebble into the middle of a pond and watching the outermost ripple expand in concentric circles at the same rate. Each time the edge of the circle touches a new grid square, we look in that grid square (see Figure 11.2).

Figure 11.2. Search grid

As it turns out, this is a well-known problem for geographical information systems (GIS) and computer graphics in general and the algorithms are well covered in many other books and papers (see, for example, Foley et al., 1995). We are not going to show any of these more complex implementations of the findNearest() method here. However, we would encourage interested readers to have a go at implementing a method in the manner described. We have also provided a "quick and dirty" implementation on the book's website: we make no guarantees about our implementation, but you may find it useful for reference or as a starting point.

There is one further issue on this topic that we have to consider before moving on. If the grid is scarcely populated, that is, there are very few matching objects in a very large grid, it may be necessary to visit a very large number of grid squares before finding a matching object. The client code may only be interested in finding entries that are within a certain distance of the originating objects, or may not have much time to wait for the response, so we need to introduce the notion of a search radius to limit the "span" of the search.

Before we move on, here is a review of the current state of the Locator class, with the code added to support the search radius.

public final class Locator{

  private JavaSpace space;
  private int searchRadius = DEFAULT_SEARCH_RADIUS;

  public Locator(JavaSpace searchSpace){
    space = searchSpace;
  }

  public void setSearchRadius(int radius){
    searchRadius = radius;
  }
  public int getSearchRadius(){
    return searchRadius;
  }

  public DeliveryVan findNearestVan(Depot depot){
    //impl
  }
  public Depot findNearestDepot(DeliveryVan van){
    //impl
  }
  private Locatable findNearest(Locatable origin,
                           Locatable template){
    //impl
  }
}

Supporting more complex searches

The next stage is to be able to find not only the nearest entry but also to find the nearest entry with certain properties. For example, one of "Packit-Shifters" customers has called up with a very large package to be transported, and we know that only certain types of vans can support this heavy load. In this case we need to support the query for not only the nearest van to the customer, but the nearest van of the necessary size.

Assuming that we have added the van's load capacity to the list of matchable fields on the DeliveryVan entry object, there turns out to be an incredibly simple solution to this and other similar problems. Here is the client-side code for the request to find the nearest delivery van with a large load capacity.

// attach the locator to the space
//and use the default search radius
Locator locator = new Locator(space);

// make a template to match the Van
DeliveryVan template = new DeliveryVan();
template.setLoadCapacity(LARGE_LOAD_CAPACITY);

// now find the nearest
DeliveryVan van =
  (DeliveryVan)locator.findNearest(aDepot,template);

Here, we are simply using the "built-in" matching properties of spaces to enable more complex searches. It has been necessary to make the findNearest() method on the Locator public in order to make a call on it from its client code. The beauty of this is that we can now support any number of more complex queries, making this searching interface very powerful, generic, and simple. One outcome of this is that the "type-specific" methods on the Locator are now redundant. They are also limiting because without them the Locator becomes useful in any number of location-based services and is not just limited to the current problem space. Here is the client code to find the nearest van to the Depot, using the generic methods:

// make a template to match the Van
DeliveryVan template = new DeliveryVan();
DeliveryVan van =
  (DeliveryVan)locator.findNearest(windsorDepot,template);

By comparing this code with the earlier equivalent example you can see that we have simply moved the construction of the template out of the Locator code and into the client, which has given us more flexibility and support for far more powerful searches. Also, by constructing the template in the client code we can support the reuse of template instances or snapshots, along with other reductions in the general complexity of the design. Of course, if we want to keep the more type-specific methods, we could create a new, more specific class for our problem domain that makes use of the generic Locator "under the hood".

Here is our final view of the Locator class:

public final class Locator{

  private JavaSpace space;
  private int searchRadius = DEFAULT_SEARCH_RADIUS;

  public Locator(JavaSpace searchSpace){
    space = searchSpace;
  }

  public void setSearchRadius(int radius){
    searchRadius = radius;
  }

  public int getSearchRadius(){
    return searchRadius;
  }

  public Locatable findNearest(Locatable origin,
                               Locatable template){
  //impl
  }
  public Locatable findNthNearest(Locatable origin,
                         Locatable template,int nth){
  //impl
  }
}

As you can see, we have removed all the type-specific methods to make the Locator applicable in all location-based searches, and we have also added a method findNthNearest(). This will allow us to find, for example, the second nearest entry in the space which matches the template, if we set the nth parameter to 2. Again, we encourage interested readers to try out this method.

By iterating over the design of the Locator, we have reached a flexible and powerful way of doing location-based queries on a space. However, there are also things that we can do to speed up the performance of searches by using the multiprocessing properties of a space.

Optimizing searches

As we discussed earlier, the approach we have taken to performing location-based searches on the space lends itself to parallelization, so we can make use of the inherent properties of spaces to speed up our searches.

Taking our implementations of the findNearest() method we can see that making the call to search the current grid square only will not be helped by making parallel calls on the space. However, searching in the north, south, east, and west, directions could all be performed in parallel. This could be a real help because for each call to the space we have to send the template entry to the space, wait for the space to perform the lookup, and then wait for the return before we set off on the next "round trip" call. If we made all four calls together on separate threads we could make the space perform four lookups at the same time. An important consideration here is that it doesn't matter which of the four lookups returns first with a valid entry because they are all considered to be equidistant from the origin object (Figure 11.3).

Figure 11.3. Concurrent searches

If nothing is found on our four parallel threads the next lookup is to the diagonally adjacent squares, and we could reuse our four threads for this search. As our circle increases we also find that in some cases we can use the "eight-way symmetry" of circles to look up the eight equidistant squares at the same time. For this purpose we could use eight parallel threads to search for matching entries and, while we wait for replies, we could work out the location of the next four or eight adjacent grid squares.

Spotting these types of optimizations can give us some major performance gains. In this case we would expect (very roughly) a five-fold increase in the performance of our search systems, depending on the setup of the network, machine loading, radius of the search, etc. This means that we could use our current machinery to perform far more searches on clients' behalf and make replies timelier and hence far more useful to our mobile services.

Driving parallelism to the extreme, we could perform optimistic lookups on the space; that is, look not only at the (currently) nearest four or eight grid squares, but also those beyond. For example, we could examine whole blocks or regions in parallel: however, this can lead to problems in making sense of the replies when they come back "out of order".

As we have already discussed, there are situations in which this type of search can be very costly in terms of computer power and time; for example, when the grid is sparsely populated with matching entries. In this case, other searching strategies can be more effective.

Finally, other "client local" optimization may also be useful to reduce the load on the space's client machines. For more information on this topic, see Chapter 14 on tuning space-based systems. However, these types of optimizations are very unlikely to yield such large benefits as the parallel lookup approaches.


Summary

In this chapter we built an extremely powerful generic spatial matching system that makes use of both the polymorphic type-matching facilities of the space and value-based associative matching.

By using the underlying properties of the space and by adding a very thin layer of code, which builds the specific matching semantics required, we have built a system that is extremely scalable, while retaining the inherent parallel nature of the space.

This is just one example of building a searching strategy on top of a space. It is always important to remember that the space is not a database, and so instead of writing SQL (structured query language) that is executed "within" the database, we write Java "without" the space in the form of services to perform the queries. In this case we have ordered the searches by location coordinates, but we could also have built systems to search in time order or on any other value or set of values.

In refactoring the Locator service to make it generic, we hope that we have shown that this type of system can apply to many spatial systems. For example, this could be applicable to some of the agent-based systems that are being developed for mobile devices (cellphones) in which location-based information is used to offer users local services while they are in an area.

Finally, we have given some possible optimizations that can be implemented in a highly parallel manner. We have given some suggestions for increasing the parallelization of the searching systems: if you take any of these ideas further we would be really interested in hearing from you via the book's website.


Chapter 12. Bidding and agent systems

We are all familiar with the concept of an auctioneer calling out the prices of bids for lots at an auction. Recently, there has been considerable interest and activity around web-based auction sites. Most web-based auctions follow the standard rules of the traditional auction house, but there are very many variations to the traditional forms of auction, all aimed at helping bidders to part with their euros, dollars or yen.

Spaces are, of course, very useful for implementing auction systems in which items for sale – "lots" – are placed in the space, various clients have access to the lots, and can make a bid for a fixed amount of time – say three days. As most readers will be familiar with this type of auction, we will leave an implementation of this type of auction using spaces as an exercise for you to do.

In this chapter, we are going to take a look at a bidding system that widens the picture a little. We can use bidding systems to not only set monetary prices for items, but to also set "values" for all types of things. We can use similar processes to those used in auctions, to trade any type of commodity or resource, and this method is generally a very effective approach to a whole range of resource allocation problems.

By introducing a balanced level of competition and collaboration between autonomous agents, which share information by using a space, we are going to show you how systems can use resources in an extremely effective and dynamic fashion.


A time bidding system

In this section we are going to "trade" time or, to put it more prosaically, delivery dates. If you cast your mind back to the previous chapter and our fictitious delivery company Packit Shifters, one of the key constraints on how much business they could do was the time that it took to deliver a package or parcel.

This is a tricky problem, because it is very easy to guarantee that there will be a van available to deliver a really important package straight away, if a small army of van drivers is employed to be on stand-by. However, this is very expensive and also boring for the drivers, although it may allow them plenty of time to improve their game of canasta! However, to the manager of Packit Shifters, this is not a great benefit. On the other hand, we could simply employ a single driver and put the new delivery on the end of his delivery list, but this won't meet the needs of the customer, because the parcel needs to be delivered as soon as possible.

Obviously systems that use resources most effectively, while giving the customers realistic delivery times, sit somewhere in between these two extremes. What we are going to do here is to build a space-based system that uses a bidding mechanism to find the earliest time that a parcel can be delivered using the available vans.

Perhaps the customer can specify that she would like a parcel or packet to be collected and delivered via a web interface on the Packit Shifters website. As well as the delivery address, the customer specifies the weight and size of the parcel. This is equivalent to putting an item "up for sale" on an auction site and describing some of its properties.

Here is the entry representing the customer's request:

import net.jini.core.entry.*;

public final class DeliveryRequest
  implements Entry {

  public CustomerID _customer;    // the customer ID
  public ParcelSize _size;        // size of the parcel
  public Integer _weight;         // weight in grams
  public Address _deliveryAddress;// where to ?

  //define the null constructor
  public DeliveryRequest () { }

  // define the populating constructor
  public DeliveryRequest (CustomerID customer,
                          ParcelSize size,
                          Integer weight,
                          Address deliveryAddress) {
    _customer = customer;
    _size = size;
    _weight = weight;
    _deliveryAddress = deliveryAddress;
  }

  // … accessors …
}

Most of these fields are self-explanatory; the CustomerID uniquely identifies the customer placing this request. This will be vital later when other parts of the system wish to reply to this request. It is also worth mentioning that parcel size can be determined from a simple table that the customer uses to size the parcel they want to send. For example, small packets would be treated as letters because they can be carried in a motorcycle pannier. This table is implemented in software as a value-based typesafe constant.


Typesafe constants with spaces

The motivation for using the typesafe constant idiom is to avoid runtime errors and the additional parameter checking required when primitives or their wrapper classes are used as constants. In space-based programming an integer is often used as a key field to represent some constant value. A template is then used with the constant field set at a known value to match entries in the space. Using an integer to represent a constant means that programs need to perform range checking on entries returned from the space because some other program may have written a rogue value to the space. That is, you can't put constraints on the range of values that can be assigned to an integer field.

The canonical form of this idiom is usually expressed as follows in a local Java program:

public final class Consts{
   public static final Consts ONE=new Consts();
   public static final Consts TWO=new Consts();
   //private constructor
   private Consts(){}
}

The == operator is used to compare values. For example:

//Only Consts objects are allowed
void constExample(Consts param){
   if(Consts.ONE==param){
     /*take action*/
   }else if(Consts.TWO==param){
     /*take action*/
   }
}

This idiom is very effective in local Java programs; however, one drawback is that it doesn't work over serialization. This is because the equality checks are being done on the object references of the class's fields. So, in the case of serialization, it breaks downs because when an object is deserialized its object references will change.

The simplest solution to this problem is for our bidding system to use a value-based typesafe constant to classify the parcels.

import java.io.Serializable;

public final class ParcelSize implements Serializable {

  public static final ParcelSize LETTER=new ParcelSize(1);
  public static final ParcelSize SMALL=new ParcelSize(2);
  public static final ParcelSize STANDARD=new ParcelSize(3);
  public static final ParcelSize LARGE=new ParcelSize(4);

  // needs some state for matching
  private int _size;

  // can only be made internally
  private ParcelSize (int size) { _size = size; }

  // this constant is immutable and intractable
  // hence no get and set methods
}

This technique works well over serialization; however, it means that you have to drop the convenient use of the == operator and override and use the equals() method instead – we will look at how get around this later, but for now let's consider what would happen if we used the canonical form of this idiom with a JavaSpace.

We must use a value-based typesafe constant with a JavaSpace, not the canonical form, otherwise every value will match every other value. The canonical form fails in a different way with a JavaSpace than it does over standard serialization, because each field in an entry is serialized separately resulting in their serialized forms all being like an empty instance of the class – the same.

The downside of using value-based typesafe constants is that you have to keep writing virtually the same code every time you define a new class of constants – or do you?

You guessed it; yes, there is a solution to make your life easier when using value-based typesafe constants with spaces. The solution is built around the knowledge that in order for key fields in an entry to be matchable they must have the same serialized form. To achieve this, we have written an abstract class called SpaceSafeConstant which takes care of the problem of having to override and use the equals() method. To use this class, you simply extend it and declare your typesafe constant class in the canonical form.

public final class ParcelSize extends SpaceSafeConstant{

  public static final ParcelSize LETTER=new ParcelSize();
  public static final ParcelSize SMALL=new ParcelSize();
  public static final ParcelSize STANDARD=new ParcelSize();
  public static final ParcelSize LARGE=new ParcelSize();

  private ParcelSize(){
  }
}

Now here's the really interesting stuff – the code for the SpaceSafeConstant class.

package jsip.util;

import java.lang.reflect.Field;

public abstract class SpaceSafeConstant
   implements java.io.Serializable{

   //the internal representation of the constant
   private transient String _fieldName;

   /**
   * get the field name for this instance
   * write it to the stream
   */
   private void writeObject(java.io.ObjectOutputStream out)
        throws java.io.IOException {

       Class clazz=getClass();
       Field [] f=clazz.getDeclaredFields();

       for(int i=0;i<f.length;i++){
       try{
             int mod=f[i].getModifiers();

             if(Modifier.isStatic(mod)
                  && Modifier.isFinal(mod)
                  && Modifier.isPublic(mod)){

               if(this==f[i].get(null)){
                     String fName=f[i].getName();
                     out.writeObject(fName);
               }
             }
           }catch(IllegalAccessException ex){
               throw new java.io.IOException(ex.getMessage());
         }
     }
}

/**
 * read the serialized field name and assign to
 * _fieldName
 */
private void readObject(java.io.ObjectInputStream in)
   throws java.io.IOException{
   try{

       _fieldName=(String)in.readObject();
   }catch(ClassNotFoundException ex){
       throw new java.io.IOException(ex.getMessage());
   }
}

 /**
  * replace the deserialized instance with the
  * local static instance to allow correct
  * usage of == operator
  */
  public Object readResolve()
      throws java.io.ObjectStreamException{

      try{

        Class clazz=getClass();
        Field f=clazz.getField(_fieldName);
        return f.get(null);
     }catch(Exception ex){
        ex.printStackTrace();
      throw new java.io.InvalidObjectException(
                       "Failed to resolve object");
     }

  }
}

Looking at the SpaceSafeConstant class, you will see that it is declared abstract (so it can't be instantiated) and implements Serializable. The class declares three private methods – writeObject(), readObject(), and read Resolve() – if these methods are present in a Serializable class they will be invoked when the object is serialized/deserialized.

The technique works by identifying which field is being serialized and then writing the name of that field into the stream. By writing the field name into the stream each time an instance of a typesafe constant is serialized, the space can match on the serialized form of that field, in this case a String.

The most interesting part of the SpaceSafeConstant class is the read Resolve() method, which is invoked during deserialization. We are using this method to identify the local static instance of the object by using the _fieldname field and then returning that object. The net result of this is that we can use the == operator after deserialization.

Although we developed this technique to use with JavaSpaces, it will work equally well over standard serialization. However, care must be taken if you are using multiple classloaders, under which circumstances you will have to resort to overriding and using the equals() method for comparisons.

After that diversion into the world of typesafe constants, it is time to return to the main plot and look at agent-based designs.


Agents and spaces

Just before we move on to look at the client code for the system, it is worth spending a few moments discussing "agents". In this chapter we are going to use the term "agent" to describe the software running on behalf of both the customer and the drivers, because these services act as "agents" for the customer and drivers: this is a classic example of an agent-based system.

Agents are used to describe many software entities, and there are innumerable classifications of agents. Here, we have chosen the term because to some degree the services are acting without direct input from the person or people that they are working on behalf of. In more formal terms, these services are autonomous and, by using the space, they collaborate to reach a solution to the scheduling problem.

Figure 12.1 shows agents working with the space. Here we assume that the company currently has three drivers: Ted, Dave, and Nancy.

Figure 12.1. Agents interacting with a JavaSpace

For the purposes of this example, we imagine that each of the members of staff has a service running that manages their delivery schedule, among other things. The agents are using the space to coordinate collaboration between them, and so we need to describe a negotiation protocol implemented via the space. All these agents are going to be implemented as Jini services, so if you are not comfortable with agent-based terminology it is possible to use the term "service" interchangeably for "agent" in the following description.


The customer agent

It is relatively simple to imagine how the customer agent creates and populates the DeliveryRequest and writes it to the space. What is also important, however, is to consider the lease time for the DeliveryRequest. In this case we are going to arbitrarily pick two minutes as the time required for the delivery services to answer with a time-based bid. So it makes sense to leave this request in the space for two minutes. Here is the example code.

final long TWO_MINUTES = 2 * 60 * 1000;
final long TWO_SECONDS = 2 * 1000;

// go find the space
JavaSpace space = SpaceLocator.getSpace();

// set up a Delivery Request
CustomerID customer=new CustomerID("SGC");

Integer weight = new Integer(200);

Address address = new Address("Cheyenne Mount");

DeliveryRequest request =
    new DeliveryRequest( customer, ParcelSize.LETTER,
                        weight, address);

// write the delivery request
space.write(request, null, TWO_MINUTES);

As you can see, once the delivery request is populated with the required values it is written into the space for the driver agents to find. We have just "bound in" some example values for the entry's fields. In a more realistic case these could be set by the customer via their web browser to a back-end servlet.

We will revisit this code in a little while but for the moment let's assume that the DeliveryRequest has been correctly populated and written to the space so that now we can turn our attention to the agents that are running on behalf of the delivery company or, more specifically, the agents that represent the various drivers working for the company.


The driver agents

The pattern we have chosen for access to the space is that the driver agents simply poll the space to see if any delivery requests have arrived. Here is the code.

JavaSpace space = SpaceLocator.getSpace();

final long TWO_SECONDS = 2 * 1000;

// match any Delivery Request
DeliveryRequest tmpl =
    new DeliveryRequest();

// a simple sleeping polling loop
DeliveryRequest request = null;
while (request == null) {
   Thread.sleep(TWO_SECONDS);
   request=(DeliveryRequest)space.read(tmpl,null,0);
}

This type of pattern is extremely simple to implement and, from our experience, is the most effective at both keeping complexity and failure modes to a minimum while also using system-level resources (such as threads, within the space) very sparingly. Of course, it does consume a thread in the client but for the most part this thread is extremely "quiet". Don't be fooled by the thought of a read with a very long wait time: this could consume a thread in both the client and the spaces server for the length of the read time, and so is kind to neither the local JVM nor the spaces JVM.

TIP

Always consider a sleeping polling access pattern when immediate notification is not an overriding factor.

In situations such as this, where you know that there is flexibility in the response times, it is always worth considering a sleeping polling read or take, over a notify mechanism. Of course if you really need to know something "as soon as possible" you could also consider a remote event-based mechanism.

RULE

Notify is only as timely as your space implementation (and network) will allow – the specification does not require a minimum response time.

Before we move on, it's important to understand the balance being struck here between the designer of the overall system and the designer and implementer of the space. The JavaSpaces specification says that the space implementer is under no time constraints in replying to a notify request, so the space could call the client back a few days after an entry matching the notify 's template is written into the space, and still be specification-compliant!

However, we must assume that the implementer of the space is going to make a "best effort" response to the JavaSpaces specification's rather loose requirements for notify, in which case the space implementation is going to consume resources, perhaps threads and memory, in making sure that notify callbacks happen as quickly as possible, and managing leases on the notify requests. This is bound to have some effect on others using the space, and limits large-scale growth for two reasons. The first is obviously performance and the second is that the space also has to handle callbacks to the clients and associated possible failure modes. A side-effect is that, as systems grow, notify-based protocols tend to exhibit greater overall systems coupling, or "wiring".

Meanwhile, back to the negotiation – the DriverAgent code above has detected a DeliveryRequest and so it is now up to the DriverAgent to make a Bid for the time at which the represented driver can deliver the parcel. As we have already mentioned, the DriverAgent will have access to information that will affect this bid, such as the driver's current schedule, the location of the customer, and perhaps many other factors. In some cases, these factors are referred to as constraints and, as such, we are building a constraint-based model using all the factors that may affect the decision, so that we can make a "best estimate" of the delivery time.

In order to make this a little less theoretical, let's assume that the driver does not currently have any deliveries scheduled. In this case, on receiving a request the driver agent will find the location of the depot and the driver's current location. The agent will also find the location of the customer by using the unique CustomerID, and the delivery address is already embedded in the delivery request.

The agent will estimate the round-trip time to get the parcel to the delivery address. As we know, the agent has a couple of minutes to make a reply and so it may try to refer to an online traffic service to make a better estimate of the total delivery time, given current traffic information.

Obviously, making effective bids in a real system will include many factors and as schedules come into play the possibilities become vast. Here we will assume that the estimating is done in a set of objects hidden behind a call to a calculateEarliestDeliveryTime() method and, as we have seen, this may require access to other systems to make as informed a decision as possible.

This type of auction is called a sealed bid auction because each of the agents makes a bid that cannot be seen by the other agents and in which there is a fixed time to make a bid.

Placing bids

Having worked out the estimated delivery time, each of the driver's agents now places a bid in the space.

Here is the code for the DeliveryBid class.

import net.jini.core.entry.*;

public final class DeliveryBid
       implements Entry, Comparable {

  // the customer ID (reply address)
  public CustomerID _customer;

  // the driver's name
  public String _driver;

  // the estimated delivery time
  public DeliveryTime _deliveryTime;

  //define the null constructor
  public DeliveryBid () { }

  // define the populating constructor
  public DeliveryBid (CustomerID customer,
                      String driver,
                      DeliveryTime deliveryTime){
    _customer = customer;
    _driver = driver;
    _deliveryTime = deliveryTime;
  }

  public DeliveryTime getTime(){
     return _deliveryTime;
  }

  public void setTime(DeliveryTime deliveryTime){
    _deliveryTime = deliveryTime;
  }

  public String getDriver(){
     return _driver;
  }

  public void setDriver(String driver){
     _driver=driver;
  }

  // for a sorted set
  public int compareTo(Object bid){
    DeliveryBid dBid=(DeliveryBid)bid;
    long myTime = _deliveryTime.getUTC();
    long yourTime =dBid.getTime().getUTC();
    if (myTime < yourTime) return –1;
    if (myTime > yourTime) return 1;
    return 0;
  }
}

You'll notice that, as well as being marked as an entry the DeliveryBid is also Comparable and in order to comply with the interface it implements the compareTo() method. We are going to use this ability when the bid arrives at the CustomerAgent to sort the bids, but for the moment we have the (extremely simplified) code of the agent finding the delivery request (above) then the agent gathers the required information to make the bid and places the bid in the space.

// a simple sleeping polling loop
DeliveryRequest request = null;
while (request == null) {
   Thread.sleep(TWO_SECONDS);
   request = (DeliveryRequest)space.read(tmpl,null,0);
}

// request is in so gather required info
CustomerID customer = request.getCustomerID();
Date deliveryTime = calculateEarliestDeliveryTime(request);
String driver = driverAgent.getDriverName();

// make a bid with the info
DeliveryBid bid=new DeliveryBid(customer,driver,deliveryTime);
space.write(bid, null, TWO_MINUTES );

Of course we assume that the drivers' names are unique, because the customer's agent needs to be able to distinguish between the various agents that have made a bid. If the driver's agent places a bid before the two minutes have elapsed, it must wait for the full two minutes for the DeliveryRequest's lease to expire in the space before polling the space for the next delivery request, or it will simply make another bid on the same request.

Taking bids

Once the customer's agent places the request in the space we decided that it would have the grace to wait for two minutes for the driver's agents or any other service that wants to, to lodge a bid.

We intend to stick to that contract, but we are not going to be completely idle in the waiting time. Assuming that the agents aren't all going to wait for the two minutes before they make their bid, we are going to poll the space in the two minutes' waiting time.

In fact, what we are going to do is again use a sleeping polling pattern, but with a new twist. Here we have implemented a sleeping polling take with a deadline. In the code, you can see that as soon as the delivery request is written to the space we set up a template to look for any delivery bids and then start polling the space to find any.

// write the delivery request
space.write(request, null, TWO_MINUTES);

// set up to receive bids.
SortedSet bids = new TreeSet();
DeliveryBid tmpl = new DeliveryBid(customer,null,null );
long deadLine = System.currentTimeMillis() + TWO_MINUTES;

// poll with a deadline
while( System.currentTimeMillis() < deadLine ) {
    Thread.sleep (TWO-SECONDS);
    Object bid = space.take(tmpl, null, (long)0);
    if (bid != null) bids.add( (DeliveryBid)bid );
}

// starting with the best confirm the bids
Iterator itr = bids.iterator();
boolean bidConfirmed = false;
while (itr.hasNext() && !bidConfirmed){
   bidConfirmed = confirmBid((DeliveryBid)itr.next());
}

By starting early on this problem, we gain some advantages. By getting early access to the bids, we are able to take them from the space and pre-sort them via the SortedSet, called bids into date and time order, so after the two minutes we already have all the bids prepared and sorted . . . or do we? Consider a situation in which a number of the driver agents are waiting for some information to come back from the traffic service. The traffic service has failed, but all the agents waiting for this information have been coded to make use of the two minutes' "grace" just in case the traffic service is restored and allows the agents to make a more accurate bid, based on the better information they have received. In this case, all the agents will legitimately wait for just under two minutes before replying. In this case, our system will only at best get a few of the "late" bids before the deadline passes.

In this case we need to write another loop which ensures that at the end of the deadline all the valid bids are removed from the space and put into the bids sorted set. We leave this as another exercise (or obvious pattern) for you, depending on your mindset.

The last part of the above code takes the ordered list of bids and attempts to confirm with the appropriate driver agent that the bid has been accepted and confirmed. The protocol for confirming the bid may be to place a bid confirmation entry into the space with the driver's unique ID in the entry so that the driver's agent can match it, schedule the delivery formally, and reply that the delivery has been scheduled. Again, you might like to think about implementing this relatively simple confirmation protocol as an exercise so that the driver and customer agent can "agree" on the deal.

For our part, we are happy because spaces have once again come to the rescue of Packit Shifters. In fact, they have been able to use spaces to schedule many more deliveries than they had before. However, the boss has said that there is "just one little problem" this success has brought about. She has a suspicion that many letters and small packages are being carried by the vans. She thinks that motorcycle couriers could handle these deliveries more effectively, leaving larger packets to be dealt with by the vans, but in order to find out if she is correct, we need to collect some information.


Letter counter service

In order to confirm our suspicion that a number of the customer bids refer to letters and small packages, we need to implement a simple letter counter service. In our ParcelSize class, remember that all letters and small packages are categorized under the ParcelSize.LETTER typesafe constant. For the reasons described earlier, we can use the == operator to compare parcel sizes.

An important element not to miss is that the letter counter sleeps for two minutes after matching a new DeliveryRequest. This is to ensure that the matching entry's lease has expired in order to prevent counting any of the DeliveryRequests more than once.

Here is the letter counter service.

final public class LetterCounter{

  public static void main(String [] args) {

    try{

      final long TWO_SECONDS = 2 * 1000;
      final long TWO_MINUTES = 2 * 60 * 1000;

      JavaSpace space = SpaceLocator.getSpace();

      int totalParcels = 0;
      int totalLetters = 0;

      // match any Delivery Request
      DeliveryRequest tmpl= new DeliveryRequest();

      while (true) {
        // a simple sleeping polling loop
        DeliveryRequest request = null;
        while (request == null) {
          Thread.sleep(TWO_SECONDS);
          request=(DeliveryRequest)space.read(tmpl,null,0);
        }

        totalParcels++;
        if (request._size==ParcelSize.LETTER){
           totalLetters++;
        }
        System.out.println(
            "Total Parcel Requests :" + totalParcels);
        System.out.println(
              "Total Letter Requests :" + totalLetters);

          Thread.sleep(TWO_MINUTES);
        }

        }
        catch (Exception exp){
            exp.printStackTrace();
        }
    }
}

By counting the number of objects that match the DeliveryRequest template, we can determine the total number of delivery requests, and by checking the ParcelSize field in the delivery requests we can determine the number of letters.

As we have said, the letter counter sleeps for two minutes after matching a new delivery request in order not to count any of the delivery requests more than once. However, this shows that our system can really only process one delivery request every two minutes, or coordination problems will occur.

If we are going to accept this limitation then we must make a rule that customer agents may only place a delivery request into the space if there are no requests already in the space. Again, a simple polling read using a delivery request entry with null fields will let us know if any already exist and to wait a while before retrying to write a new delivery request into the space. However, even with this limitation, we have to be able to dynamically introduce a counting service into our agent-based bidding system, and we now have all the details we need to make an informed decision about employing a delivery bike rider.

We discussed earlier that remote events are inherently unreliable. However, as an alternative approach to the letter-counting service we could register a template as follows to notify and count the number of matching writes in a remote event listener – refer back to Chapter 7 for an in-depth discussion of remote events.

DeliveryRequest tmpl =
    new DeliveryRequest(null,ParcelSize.LETTER,null,null);

The biker agent

As it turned out, the boss's suspicions were correct and so the next stage is to hire a delivery bike rider as well as van drivers. The bike rider (who is called Andy) also now has an agent which looks very much like the driver agents, but with template matching set to ParcelSize.LETTER only, because we know that the bike rider can only carry letters and small packets, and so it is pointless to match and bid for any other loads. For the sake of completeness, here is the core code for the biker agent. You will notice that it is very similar to that for the driver agent, but is limited to matching letters only.

JavaSpace space = SpaceLocator.getSpace();

final long TWO_SECONDS = 2 * 1000;
// match any Letter Request
DeliveryRequest tmpl =
  new DeliveryRequest(null,ParcelSize.LETTER,null,null);

// a simple sleeping polling loop
DeliveryRequest request = null;
while (request == null) {
    Thread.sleep(TWO_SECONDS);
    request = (DeliveryRequest)space.read(tmpl, null, 0);
}

// request is in so gather required info
CustomerID customer = request.getCustomerID();
Date deliveryTime = calculateEarliestDeliveryTime(request);
String driver = driverAgent.getDriverName();

// make a bid with the info
DeliveryBid bid=new DeliveryBid(customer,driver,deliveryTime);
space.write(bid, null, TWO_MINUTES );

Because the bike rider can get through traffic more quickly, he will tend to outbid the van drivers on the letter-type deliveries, and for the other types the agent will simply not make a bid, leaving the "auction" open for the van drivers. So in general the driver agents will schedule fewer letters and more parcels of the larger size.

Figure 12.2 shows the system up and running with the new biker agent added. As you can see we have introduced a new type of agent without stopping the system. The introduction of this agent has meant that all types of parcels, not just the letters, get delivered more effectively and it has not been necessary to change or even stop the agents that are already running from using the space.

Figure 12.2. Additional agent interactions

Given the amount of repetition in the code for the biker and driver agents you might like to consider refactoring the two agents to be able to parameterize the types of parcels that can be handled by their associated delivery person, or you may like to consider parameterizing other constraints.


Summary

We have looked at a very simple example system in which agents bid to deliver parcels of various sizes in order to most effectively share work and distribute resources. Here we considered a very simple negotiation over a delivery time.

As we have described it, the system can really only consider one request every two minutes because it uses the CustomerID as a key, not a uniquely generated request number. You might like to consider how to make the system deal with any number of delivery requests and process them as they appear. You may also like to think about how to implement a bidding system using remote events to coordinate the activities of the agents (but always bear in mind that remote events are not guaranteed to be delivered: how may this affect the operation of such a system?)

By holding a number of requests at the same time and working on some very clever routing details against the current drivers' schedule, the agents may be able to make some very efficient bids by adopting "rerouting" strategies.

We have also made the assumption that our delivery time bidding is accurate in that the van drivers can actually deliver the parcels within the bid time. In order to make sure this is the case, we also need to check the actual delivery log as the customers sign for the packets to make sure that the two match. If deliveries aren't going to schedule, we need to rethink the scheduling systems. Again this is a very interesting problem, but one we won't discuss here.

Another key assumption is that the bidding is fair, in that all the drivers are using the same scheduling system and each has the same amount of information available to it, for example traffic information, route tracking, etc.

We have been able to run live experiments on the space to observe its behavior and verify certain ideas that we had. Given this information, we were able to dynamically introduce new agents. During these activities, deliveries were still scheduled and parcels were still delivered: this is a very elegant and dynamically extensible solution to a resource allocation problem.

As the system stands, we are using the whole of the space as an auction house, which works well for our simple system but if you intend to develop the system further you may like to consider introducing the concept of a specific "agora" or trading forum in which the agents involved in the bidding protocols are known. This can help with any timing and coordination problems that you may encounter. Just as with distributed systems, the two key elements are that the agents are able to find the auctions that are occurring and are able to join the interactions to place bids or replies.

As ever, we have just touched on the use of agents in spaces and what may be achieved with various auction protocols. These types of system are particularly good at dealing with problems such as scheduling and routing, which can be shown to have no perfect solution – so-called NP complete problems. In this chapter we have used a space to share out information regarding a particular problem to allow separate processes to work together to solve the problem in an elegant and scalable way.


Chapter 13. Testing space-based systems

In many cases, building distributed systems with JavaSpaces allows us to reach solutions quicker than we would with "traditional" techniques, and with the right approaches many of the problems of building distributed systems simply disappear with the use of JavaSpaces.

For example, spaces allow us to pull systems apart and distribute them more simply than most other techniques and tools, but this can lead to a lower level of predictability and confidence in the overall behavior of the system. We may have to give up some control and visibility over the activities of the system as a whole.

When we deploy space-based systems, we need to feel confident that they will work, and continue to work as we scale them up.

TIP

Build and test a small system first to test out your assumptions.

Overall, our suggested approach to this is first to always build a small subset of the system that you intend to deploy in order to test your assumptions of its behavior against what you can observe. It is very easy to make too many assumptions at design and build time that are not borne out in practice. We have made, and continue to make, many such errors based on false assumptions about the way a space-based system will operate; mainly because in practice systems built with spaces have a very different character to those built with traditional distributed tools.

Even with small systems, finding out what is going on "in the space" can be hard. Shifting reads, writes, and takes around in your code, or inserting debug statements in "important" places may give you clues, but in our experience this has only been effective in the simplest of systems, where it is easy to imagine the flow of objects around the system. In our experience, systems of anything other than the most trivial complexity require us not only to stretch our imaginations but also to use tools to help us image what is going on. Tools that give systems developers or designers a view of the activities of the various clients with the system can help address visibility problems and restore confidence in understanding the system's behavior as a whole.

In general, software debuggers apply to a single executable and allow developers to observe the behavior of the software that they have written. So, debuggers are not only used to debug systems but also to understand the way they work, or provide clues for refactoring of code, and also perhaps to optimize the behavior of the "local" system.

When we apply this model to spaces, we need a tool that allows us to see the behavior of a distributed system as a whole. In the same way that local debuggers allow us to see the way a "local" program behaves and what individual threads are doing, we need to see what each of the clients of the space is doing in order to see how the whole system is behaving.

One of the most useful tools is a logging system that allows us to record the operations being performed on the space's API. Later, we can analyze the log to see which operations have been performed and in what order. Other spaces users have found this technique helpful too and have come up with various solutions for independently constructing a logging system.

In this chapter, we are going to build a system that will allow us to "invisibly" record activities of space clients by producing "space events" and logging them for analysis immediately or later on. We are going to concentrate on the collection of events associated with JavaSpaces API calls, rather than the way that we view and analyze the events.

In this chapter, we are going to develop an architecture for the system over a number of iterations and show how various architectural models may be used to implement the logging system. On the way, we will examine various forms of proxies and take a look at the way Jini uses proxies to distribute systems, so we are going to assume that you have a basic knowledge of Jini and how the lookup system works. We're also going to analyze the JavaSpaces API in terms of an object model.

We are going to give you a flavor of a number of implementations of a logging system. By characterizing each solution, we can improve our design generally and understand situations in which the various solutions might be useful. We are also going to use some of the dynamic features of the Jini environment, but we will not go into all the Jini details, as this book is predominately about JavaSpaces. However, we are going to use some Jini features, such as proxies, so being familiar with some of the basic Jini features would help, but is not essential.


A space logging system

One of the main benefits of a logging system is that it allows us to see how clients are using the space without having to insert "bespoke" debugging instructions into our client code. In order to automate this process without touching our client code, we are going to use a very simple but powerful idea.

We know that all space-based systems load a proxy at runtime in order to support remote calls on the JavaSpaces API. Using this knowledge, we can easily build new proxies that hide logging operations from the client, while still looking exactly like the standard JavaSpace proxy. So, in our system, the Jini proxy for our logging system will implement the JavaSpaces interface but, instead of making the calls to the back-end of the JavaSpace directly, the proxy will collect the information that we need and then forward the JavaSpaces API calls on to another proxy that uses the back-end JavaSpaces server.

In order to avoid confusion, in this chapter we are going to call the proxy that produces logging information the LoggingProxy. The standard proxy that calls into the back-end of the space is called the DelegateProxy and the combination of the DelegateProxy and the back-end space will naturally be called the DelegateSpace.

Figure 13.1 shows the general structure that we will use for building the Logging Proxies and using the Delegate Space.

Figure 13.1. Local logging proxy

To express this in code, the interface to the Logging Proxy is exactly the same as the JavaSpaces interface, but we can use a type-based lookup to find a Logging Proxy.

public interface LoggingSpace extends JavaSpace{
}

Now that we have established a general structure for the Logging Proxy let's consider for a moment how we are going to record and structure the logging information.

Logging information model

We have two candidate object models for our logging information. The first is a class- or type-based model given in Figure 13.2. A quick view of the JavaSpaces interface has generated the inheritance graph, which represents various operations that we might record in the logging system.

Figure 13.2. Space operations hierarchy

As you can see from the class hierarchy, there is an operation class for every method call in the JavaSpaces interface, and others to deal with more general or abstract information.

This model has the advantage that only the information that applies to a particular type of operation will be stored and recorded in the object representing that operation, and it has the standard property of extensibility and flexibility that is normally associated with type-based inheritance systems. However, it does add complexity and raise the class count dramatically.

The other alternative is to hold all the method information and parameters in a single class using null information for fields that are not used for a particular operation. In this case, we need to add a field to represent the operation that is being called instead of using the Java-type system to "encode" the operation. Here is the Java code for a simple compound SpaceOperation class.

public class SpaceOperation

    implements Serializable {

          private long _timeStamp;
          private String _host;
          private String _thread;
          private String _method;
          private String _entry;
          private String _txn;
          private long _timeVal;
          private static String _hostCache;
          publicSpaceOperation(String method,Entry entry,
                       Transaction txn,long timeVal){

              _timeStamp=System.currentTimeMillis();
              _host = getHostDetails();
              _thread= Thread.currentThread().getName();
              _method= method;
              if (entry != null){
                _entry = entry.getClass().getName();
              }
              if (txn != null){
                _txn = txn.toString();
              }
              _timeVal= timeVal;
            }

            private String getHostDetails() {

              if(_hostCache==null){
                try {
                   InetAddress host=
                     InetAddress.getLocalHost();
                   _hostCache=host.toString();
                }catch (java.net.UnknownHostException exp){
                   _hostCache="No Host Details";
                }

          }
            return _hostCache;
          }

          private static final char comma=',';
          private static final int BUF_SIZE=128;

          public String toString(){
              StringBuffer buf=new StringBuffer(BUF_SIZE);
              buf.append(new java.until.Date(_timeStamp));
              buf.append(comma);
              buf.append(_host);
              buf.append(comma);
              buf.append(_thread);
              buf.append(comma);
              buf.append(_method);
              buf.append(comma);
              buf.append(_entry);
              buf.append(comma);
              buf.append(_txn);
              buf.append(comma);
              buf.append(_timeVal);
              return buf.toString();
          }

    }

Every time a call is made on the JavaSpaces API in a Logging Proxy it is possible to create a new instance of the SpaceOperation class to log the type of operation and the parameters to this operation. The class also overrides the toString() method of the Object class to set out the information in a comma-separated format, so that it is simple to parse into other software tools.

So this single class model seems adequate for our purposes, while having the benefits of being simple and lightweight. Now that we have a way of recording and formatting our logging information, we can look at our first implementation of the Logging Proxy.


A local Logging Proxy

Using the general architecture shown in Figure 13.1, one of the simplest implementations would be to write out the logging information on the local console or to a local disk, so the information is recorded locally to each client making calls on the space proxy.

When a call is made to the Logging Proxy's API, the Logging Proxy constructs a SpaceOperation object for the associated JavaSpaces API and then forwards the call to the Delegate Proxy. For simplicity's sake, the logOperation() method simply uses the system's output stream to write the formatted SpaceOperation object to the local console; but it could be implemented to write to a local disk file.

Here are the highlights of the LocalLoggingProxy implementation:

public class LocalLoggingProxy
    implements LoggingSpace, Externalizable{

   private JavaSpace _delegateSpace;

   public void readExternal(ObjectInput in)
       throws IOException,ClassNotFoundException{

   //nothing to read
   Debug.message("readExternal() finding space");
   try {
       _delegateSpace=SpaceLocator.getSpace();
   }catch (Exception exp){
       throw new IOException(
           "Can't find JavaSpace "+exp.toString());
      }
  }
  public void writeExternal(ObjectOutput out)
     throws IOException{
      //nothing to write
  }
  public Lease write(Entry entry,
         Transaction txn,long lease)
     throws TransactionException,
           RemoteException {

     logOperation(
       new SpaceOperation("write",entry,txn,lease));

     return _delegateSpace.write(entry, txn, lease);
  }

  // … all of the other JavaSpaces API methods …

  public Entry snapshot(Entry entry)
       throws RemoteException {

     logOperation(
       new SpaceOperation("snapshot",entry,null,0));

     return _delegateSpace.snapshot(entry);
  }
  private void logOperation(SpaceOperation op) {
     System.out.println(op.toString());
  }
}

We have skipped most of the methods that exist in the JavaSpaces API because they follow basically the same pattern as the write method. In the case of snapshots we have included default information for some of the parameters.

The most interesting thing to note in the LocalLoggingProxy class is how it dynamically embeds the Delegate Space Proxy. Because we know that when the proxy is deserialized by a client the readExternal() method is invoked by the Java's serialization mechanism, we can insert the code to find the Delegate Space and assign its proxy to the field _delegateSpace, which is used by the LocalLoggingProxy to invoke the matching methods on the JavaSpace.

We can safely use the readExternal() technique to locate the space because we know that the proxy gets serialized to the Jini Lookup Service (LUS) as a MarshalledObject, meaning that the proxy itself doesn't get deserialized in the LUS. If this wasn't the case, using this technique would be a bad idea because the readExternal() method would also be invoked in the LUS.

Before moving on, let's look briefly at the code required to register the proxy with the Jini LUS.

Registering the proxy

One important thing to note in the code to register the proxy is that, as the proxy is a simple serializable object and not an RMI object such as a Unicast RemoteObject, we must have a loop at the end of main to stop the JVM exiting. This is because if the VM exited the JoinManager wouldn't be able to renew the proxy's lease in the LUS, resulting in the proxy being removed from the LUS when the lease expired.

import jsip.debug.Debug;

public class RegisterProxy {
    private static JoinManager _joinManager;

    public static void main(String [] args){
       try{

       System.setSecurityManager(
         new java.rmi.RMISecurityManager());

       // create a proxy to put into Reggie
       LocalLoggingProxy proxy = new LocalLoggingProxy();

       //join stuff, ignoring persistent service id for now.
       ServiceIDListener listener=new ServiceIDListener(){
         public void serviceIDNotify(ServiceID serviceID) {
           //not storing for now
           System.out.println("serviceIDNotify()");
        }
      };

      Entry [] attribs=new Entry[]{
            new Name("Local Logging Space")
      };

      _joinManager=
          new JoinManager(proxy,attribs,listener,null,null);

      //keep the VM alive, as we're not using
      // a UnicastRemoteObject.
      //if the VM exits then the proxy's
      //lease with the LUS would expire
      while(true){
           Thread.sleep(10000);
    }

    }catch(Exception ex){
        ex.printStackTrace();
    }
  }
}

A second point to note in the RegisterProxy example is that we are not storing and reusing the ServiceID provided by the LUS. This is to keep the example simple: storing and reusing ServiceIDs is something you should always do. The simplest way to do this is to serialize ServiceIDs to the local file system, so that each time you want to register a proxy the code can check for the existence of the file and use the stored ServiceID if one exists. Failure to reuse ServiceIDs results in "dead" proxies residing in the LUS until their leases expire. In contrast, by reusing a ServiceID the LUS will replace the matching proxy with the new one.


Reviewing the design

The LocalLoggingProxy may work very well if you are simply testing a single client on a single machine, if for example you are using the space as a simplified persistence mechanism for a Java application. However, a local logging proxy for a distributed system doesn't make sense, so we need to consider a couple of the potential problems with the implementation thus far. First, if we are running clients on various machines we have to have some way of collecting up the information to get an overall view of the system's behavior. This means that we need to have access to all the local files in order to merge them to get a coherent overall picture, which is an arduous task.

Second, the way that Jini distributes proxies means that we can't predict where the space's proxies are going to run, and if we intend to send our logging information to a disk, for example, there is no guarantee that a disk will be present on the device running the client and proxy, or that the device will be capable of storing more than trivial amounts of debugging information.

However, we do know that the system must have some kind of network connection or the proxy could not have arrived in the first place (unless the local machine is running a lookup service and the space is running locally) and so we could build the proxy to move the information back over the network on to a machine that we know has adequate resources.


The JavaSpace logging service

In our second model, we retain the Logging Proxy interface but reimplement the proxy to provide its information to the centrally located server. However, there are a number of issues involved in building a logging server that has to deal with network latency and partial failure, so it makes sense to build the logging service as a Jini service.

The role of the logging service is to store the SpaceOperation information in a centralized repository. The repository could be a database, a file, or even another space. For simplicity's sake we are going to write the data to a CSV (comma separated delimiter) file.

Figure 13.3 shows the overall structure of the architecture that uses the logging service.

Figure 13.3. Logging service

In this second model, the Logging Proxy client is going to perform two remote calls per JavaSpace method; one to the Delegate Space and one to the logging service. Obviously, making two remote calls is going to slow down the overall performance of a system; however, we only envisage the logging service being used for debugging or during development, not on a production system, so the overhead is acceptable.

Logging interface

Having got a design in mind, the first thing we need to do is come up with the interface for the remote logging service. This is actually quite simple – all we need is a logOperation() method that takes a SpaceOperation parameter in the same way we did in the LocalLoggingProxy, but in this instance we define the method in a Remote interface because later, when we write the new proxy, it will delegate calls to the remote logging service.

public interface SpaceLogger extends Remote{
    public void logOperation(SpaceOperation op)
      throws RemoteException;
}

Logging strategies

Earlier we mentioned that various strategies could be used to store the Space Operation data and, for the purpose of the examples in this chapter, we would use a simple CSV file implementation. However, since other strategies exist it makes sense to define an extra local interface, LoggingStrategy, to make it easier to change strategies later on.

The design we are going to develop here uses delegation to forward the logOperation() request from the SpaceLogger implementation to the appropriate LoggingStrategy. Not only does this simplify writing the SpaceLogger implementation, it will also allow us to dynamically load the desired strategy when we start an instance of the logging service.

Here's the interface we've decided on for LoggingStrategy, which is non-remote, but also defines a logOperation() method.

public interface LoggingStrategy{
    public void logOperation(SpaceOperation op);
}

The UML diagram shown in Figure 13.4 shows the relationship between the SpaceLogger and the LoggingStrategy interfaces. The figure also shows how the SpaceLoggerImpl class (which we will develop next) delegates to one of the LoggingStrategy implementations.

Figure 13.4. Space logger class diagram

With the class model for the logging service completed, we can finally start to get our hands dirty and write some real code!

Logging implementation

First, we need to write a class that implements the SpaceLogger interface that can load the appropriate LoggingStrategy.

public class SpaceLoggerImpl extends UnicastRemoteObject
    implements SpaceLogger{
    private LoggingStrategy _impl;

  public LoggingServiceImpl(String strategyClassName)
      throws RemoteException{

      super();
      try{
        Class c=Class.forName(strategyClassName);
        _impl=(LoggingStrategy)c.newInstance();
      }catch(Exception ex){
        throw new
          RemoteException("Failed to load
                         LoggingStrategy:",ex);
      }
  }
  public Object logOperation(SpaceOperation op)
      throws java.rmi.RemoteException{

    return _impl.logOperation(op);
  }
}

If you step through the code, you will see that because SpaceLoggerImpl is going to be used as a remote service it extends java.rmi.server.Unicast RemoteObject – this allows it to receive remote calls from the proxy. Most of the interesting stuff occurs in the constructor, which uses the argument strategyClassName as the class name of the LoggingStrategy. The method continues by using Class.forName() to load the strategy class and uses newInstance() on the Class object to instantiate it. Here we make the assumption that all LoggingStrategy implementations have a public noargs constrcutor.

Finally, in the logOperation() method, the op parameter is forwarded to the LoggingStrategy class which was loaded and instantiated in the constructor. As you can see, by breaking down the design into a logging service and logging strategies we have arrived at a very simple service that is extensible and easier to maintain because the detail of how the logging occurs is specific to the concrete LoggingStrategy classes, rather than embedded in the service class.

CSV logging strategy

A simple strategy for logging space-based operations is to write them to a CSV file. This will allow us to easily view the log file by opening it in a spreadsheet application – most spreadsheets will open CSV files and use the comma delimiter to determine where each column starts and ends, and parse a carriage return as a marker for the end of a row.

Previously we implemented the toString() method in the SpaceOperation class to return a CSV-formatted string, so we can quickly come up with a very basic LoggingStrategy implementation as shown below:

public class CSVStrategy
   implements LoggingStrategy{

   public synchronized
       void logOperation(SpaceOperation op){

   try{

      String msg=op.toString();
      RandomAccessFile raf=
              new RandomAccessFile("spacelog.csv","rw");
      FileOutputStream fos=
              new FileOutputStream(raf.getFD());
      raf.seek(raf.length());

      PrintWriter pw=new PrintWriter(fos);
      pw.println(msg);
      pw.flush();
      fos.close();

   }catch(Exception ex){
      ex.printStackTrace();
      }
   }
}

Although our simple implementation will work, the logOperation() method must be synchronized to avoid corrupting the log file or miss writing data to the file due to IOExceptions. The obvious problem with this implementation is the risk of lock contention caused by multiple clients (or threads in the same client) blocking writing to the log file.

A simple, but effective, solution to reduce lock contention is to use a guarded queue to store the SpaceOperation objects as they arrive and to have a separate thread processing the head of the queue, thus writing the Space Operation object out to disk.

Using a guarded queue only causes contention for the duration of adding or removing data from the queue, which in almost all cases is shorter than the length of time it takes to write the data to the file. If you're not familiar with how a guarded queue works, then the one we are going to use in the next code example is shown in Appendix A, and is available as part of the utility class library that you can download from the book's website.

Below is a revised version of the CSVStrategy class using a queue. The dump() method has the same code as the previous version's logOperation() method.

public class CSVStrategy
   implements LoggingStrategy, Runnable{

   private jsip.util.Queue _queue
                     =new jsip.util.Queue();

   public CSVStrategy(){
      Thread t=new Thread(this);
      t.start();
  }

  public void logOperation(SpaceOperation op){

     _queue.add(op);
  }
  public void run(){
    while(!Thread.currentThread().isInterrupted()){

      SpaceOperation op=
        (SpaceOperation)_queue.getNext();
      //write the SpaceOperation to disk
    dump(op);
    }
  }
}

Now that we have a LoggingStrategy implementation, we can start to put together the final pieces of the jigsaw, which are the proxy and service starter classes.

Remote Logging Proxy

Earlier we looked at the LocalLoggingProxy class, which implemented our LoggingSpace interface and simply wrote the logging information to the local JVM console. Much of the code in the LocalLoggingProxy is the same as we are going to need for our logging service, except that the call to log Operation() will delegate to the SpaceLogger rather than writing to disk. For this reason it is worth defining an abstract class that both the LocalLoggingProxy and LoggingServiceProxy can extend.

public abstract class AbstractLoggingProxy
    implements LoggingSpace, Externalizable{

private JavaSpace _delegateSpace;

//readExternal() same as in original code
//writeExternal() same as in original code

public abstract
  void logOperation(SpaceOperation op);

public Lease write(Entry entry,
      Transaction txn,long lease)
  throws TransactionException,
         RemoteException {

  logOperation(
    new SpaceOperation("write",entry,txn,lease));

  return _delegateSpace.write(entry, txn, lease);
  }
  //all the other JavaSpaces methods
}

The LocalLoggingProxy would then simply look as follows, as its superclass takes care of finding the Delegate Space and forwarding the calls.

public class LocalLoggingProxy
    extends AbstractLoggingProxy{

  public void logOperation(SpaceOperation op) {
    System.out.println(op.toString());
  }
}

Our new LoggingServiceProxy would look like this:

public class LoggingServiceProxy extends AbstractLoggingProxy
    implements Serializable{
  private SpaceLogger _logger;

  public void readExternal(ObjectInput in)
       throws IOException, ClassNotFoundException{

       //will find the JavaSpace
       super.readExternal(in);

       try {
         //now locate the logging service's proxy
           _logger=(SpaceLogger)
              ServiceLocator.getService(SpaceLogger.class);
       }catch (Exception exp){
         throw new IOException(
             "Can't find logging service "+exp.toString());
       }
    }
    public void writeExternal(ObjectOutput out)
         throws IOException{
       super.writeExternal();
    }

    public void logOperation(SpaceOperation op)
         throws RemoteException{
           _logger.logOperation(op);
    }
}

We are using the same technique of locating the delegate services in the LoggingServiceProxy class (i.e. when the proxy is deserialized) as we did in the LocalLoggingProxy. This allows us to register the proxy without having to locate and bind to the delegate services at that time.

The final stage is to write the class that registers the logging service.

Registering the Logging Service Proxy

Earlier we saw how to register the LocalLoggingProxy to the Jini Lookup Service. As the code for registering the LoggingServiceProxy is identical to this, except the proxy's class and Name attribute, we won't repeat it here. However, we don't want to miss the opportunity to comment on how to make some existing code reusable, so you should consider using the Class.for Name() technique in the RegisterProxy class to load and register the appropriate proxy based on a parameter to main().


Alternative designs

In both the logging models we have looked at in this chapter we have used a late binding technique to find the delegate service at the point that the proxy is deserialized in the client. An alternative approach is to have the RegisterProxy class locate the required service or services and initialize the proxies with them. This approach has the advantage that a proxy will only be registered, if the delegate service can be found; however, if any of the delegate services are stopped and restarted after the proxy has been registered, the remote reference (to the delegate service(s)) would become invalid, unless of course, they are activatable services.

Enhancements

There are few important enhancements that can be added to the code we have presented in this chapter. The most obvious enhancement is to pass the space name and logging service names to the proxy or proxies to identify the correct services – this will allow you to have multiple logging services and spaces running with the Jini community.

A more advanced enhancement is to make the logging service a "well-behaved Jini service", thus supporting the JoinAdmin interface, which allows the remote configuration of groups, lookup locators, and attributes.

Logging information based on the toString() representation of the parameters passed to the JavaSpaces API may not be sufficient. You may therefore like to consider building on the concepts introduced in this chapter by modifying the SpaceOperation class to include fields that contain the actual parameters as objects, e.g transactions and entries. This would allow an advanced logging strategy to log transaction IDs and use reflection on the entries to store more informative information.

Other enhancements could be to record the duration of the call to the JavaSpace and store it in the SpaceOperation class, or to store the return value from the call to the JavaSpace. The latter is a useful logging enhancement for determining whether a read or take operation has succeeded.


Summary

Although using JavaSpaces simplifies many aspects of writing and deploying distributed systems, debugging can be tricky. Some implementations of spaces have both debugging modes and debugging tools. If you have these tools available, we suggest you use whatever you find easiest and that which gives you the most information.

Always build a small subset of the system that you intend to deploy in order to test your assumptions of its behavior against what you can observe.

A simple local logging proxy can be useful for single-client testing, but is more trouble than it's worth when multiple clients are involved.

Using a JavaSpaces (Jini) logging service along the lines described in this chapter can help to identify problems more quickly (in some cases) than writing application-specific debugging code. Implementing a JDBC strategy will allow you to use SQL to query the log and filter on particular method types, hosts, or template parameters.


Chapter 14. Tuning space-based systems

In Chapter 13 we looked at how to build logging systems to allow us to examine distributed space-based systems to check that they were working properly. In this chapter we are going to look at how to build efficient and effective clients of a space. By this we mean that they use the resources available to them (in terms of the systems hardware) effectively, so that they go not only faster but also use memory and other computer resources well. Remember that by using a space, clients are also consuming resources in the space server and so it is important that they share the resources of the space server, such as threads, in a cooperative manner.

RULE

Tune a system once it is working – never before!

First, we strongly suggest that you treat the development of all your systems in the same order; that is, build something simple and make it work before tuning it up.

As a rule, try to think about tuning a distributed system as improving the overall object flow or object throughput of the entire system. After all, entries only become useful once they have arrived at a client where information or behavior, or both, can be used to modify their actions.

TIP

Analyze your tuning effectiveness by measuring the total object throughput at a system-wide level.

Analyzing timestamps on the output of the logging system will give you indications of the effectiveness of your tuning activities, and remember that making one part of a system faster may make other parts of the system slower, which is why we suggest that you do not just analyze effects locally but at a system-wide level.

In the following sections we are going to introduce some tuning techniques and give some tuning tips that you can apply generally. Let's start with a simple example of a lightweight distributed counter.


A lightweight counter

Let's take a look at the problem of managing a lightweight distributed counter. In this case we have implemented a simple system for collecting usage information from a number of web servers, each of which uses the space as the central point for holding system-wide information. For example, a number of counters are held in the space so that we can record items such as page hits, cache refreshes, etc. across all our web servers.

We hold an instance of the counter for every type of activity that we want to record. Here is the Counter Entry class.

public class Counter
   implements Entry{

   public String _activity = "Undefined";

   public Integer _count;

   //null constructor must be defined
   public Counter() { }

   public Counter(String activityName){
      _activity = activityName;
      reset();
   }

   public final void increment() {
      _count = new Integer(_count.intValue()+1);
   }
   public final void reset() {
      _count = new Integer(0);
   }
   public String getActivity(){
      return _activity;
   }
   public int getCount(){
      return _count.intValue();
   }
}

The code below shows how each of the web servers finds the appropriate counter for the activity. To record the activity, they increment the counter. In this example we are incrementing the number of page hits.

class PageServer{

  private JavaSpace _space;

  public void servePage(PageHandle handle)
     throws Exception /*keep example simple*/{

     Transaction txn=
       TxnManager.createTxn(ONE_MINUTE);
    Counter template = new Counter("PageHits");
    Counter counter
      =(Counter)_space.take(template,txn,WAIT_TIME);

    if (counter != null) {
      counter.increment();
      _space.write(counter, txn, WRITE_LEASE );
      txn.commit();
    }else{
      txn.abort();
    }
    //serve up page
  }
}

Although this example is very simple, the important point to note is that we are keeping the time between the take on the Counter entry and the write back to the space as short as possible. This increases the entry's availability to other parts of the system. Holding on to an entry once it has been taken means that other clients are going to have to wait longer for the entry to become available. Here, this would mean that we would have to start increasing the WAIT_TIME constant in the above example to give ourselves a really good chance of taking the entry.

TIP

Make the time between the take and the write of an entry as short as possible.

So if you find yourself running around the system increasing wait times you might also like to check how long clients are holding on to entries, and make these times as short as possible.

If you have a lot of processing to do on an entry that also needs to be highly available, one idiom we sometimes use is to first read the entry and make a clone of it while we keep hold of the original entry. The clone is then used to perform the processing. Next, take the entry from the space based on the original entry, to ensure that the state of the entry has not been changed in the interim. If they match, then simply write the clone into the space. If they don't match, then you have to start the whole process again.

This technique works particularly well for systems in which users are viewing data gleaned from the space on a graphical interface with a view to modifying it. In this case, one would attach the cloned entry to the viewing screen so that the user could modify it locally, and retain the original read entry. Normally such viewers have an Apply or OK button to make the changes, which in this case means take and write the cloned entry (unless the entry has changed). There may also be a Cancel button which simply throws both the clone and the read entry away ready for garbage collection.

There are situations where this idiom can be very effective and others where it can be counterproductive. Which is which depends on the object usage patterns for particular systems. You might like to apply this pattern as a tuning experiment once the overall system is running.


Tuning local code

Our counting system is running well, but as you've probably already guessed, the web servers will be running more slowly than they did before, due to the additional overhead of recording the number of page hits. Although we are concerned with the number of pages that we have served, the clients of the web servers want to see their information as soon as possible, so let's look at a couple of techniques that we can use to speed things up a bit.

The first area we should focus on is identifying the location(s) of bottlenecks in the system. In this example, that's easy – it is the space, and that's why we chose this example. So we know that using a space to store the counter information has slowed down the overall performance of the system, but why is this?

TIP

Using lazy updates can boost performance.

Let's ignore the overhead of the remote calls on the space and concentrate on the PageServer 's usage of the space. As it stands, each time a page is requested the PageServer goes to the space, takes, updates, and then writes back the counter entry. This creates a point of contention if multiple pages are requested concurrently. A take, update, write sequence for a single entry is analogous to entering a synchronized block of code.

To reduce the contention for our entries we can perform lazy updates on the space. This can be achieved by using something like a local queue to hold the page request information and having a thread process the queue and perform the updates on the space in the background.

By using one or more local queues, the lock contention duration can be reduced dramatically as the point of content has changed from a take, write sequence to a single local synchronized method – adding data to the queue.

class PageServer{

  private Queue _queue=new Queue();
  private JavaSpace _space;

  PageServer(JavaSpace space){
    _space=space;
  }

  public void servePage(PageHandle handle){

    _queue.add(handle);
    //serve up page
  }
  public void run(){

    //loop plus exception handling omitted

    //take head from queue
    PageHandle h=(PageHandle)_queue.getNext();
    String pageName=h.getName();
    //update the page hits first
    Transaction txn=
      TxnManager.createTxn(TXN_LIFETIME);

    Counter template = new Counter(pageName);
    Counter counter
      =(Counter)_space.take(template,txn,WAIT_TIME);

    if (counter != null) {
      counter.increment();
      _space.write(counter,txn,WRITE_LEASE );
      txn.commit();
    }//else abort

    //update global counter
    //update other bits
  }
}

This technique is a good optimization, but comes at a price – if the process using the queue crashes, the information held in the space might not be up to date.

The second thing we can focus on is at the local JVM level; object creation and garbage collection. Although this isn't a space-specific technique, it is worth mentioning in the current context. By moving the construction of the template away from the main code (as shown in the previous example) we can stop cycling the constructor, associated resource allocations, and the garbage collector every time we hit a new page.

class PageServer{

  private Counter _template=new Counter("PageHits");

  //as before, but using _template
}

This has helped us in a general way to regain some performance by using a standard Java optimization, and there are many more ways of doing this. Dov Bulka gives many specific examples for tuning server-side systems and web servers in particular. You can find a reference to his book in the references section on page 238. We will now move on to space-specific techniques for tuning up your distributed system.


Using snapshot

Snapshot is an optimization method. It's the only one that you will find on the JavaSpaces interface. Its is put there so that JavaSpace implementations can improve the object throughput on the assumption that clients are going to use an entry, normally a template, a number of times without its state being altered. Another way to view snapshot() is that it is a way for client programmers to hint to the space that they are going to repeatedly use a particular template.

Here is a first try at using snapshot to increase the performance of our system. We have moved the initialization code from "inline" to the constructor in our page server class in order to create the template only once.

Counter slowTemplate = new Counter("PageHits");
//incorrect use of snapshot
_template = (Counter)space.snapshot(slowTemplate);

If you are not familiar with the snapshot() method, the above code probably looks correct: that is, we're casting the return type to the entry type we expect, in this case Counter. However, the first things to remember with snapshot is that the concrete type returned is undefined, so in your client code you should only refer to it as an entry.

RULE

The type of template returned from a call to snapshot is not defined and should not be assumed.

Of course it is possible that certain implementations could return the same concrete type from snapshot as the type of entry used for the template; however, it is not safe to write code that makes this assumption.

So in our code, we can use the entry interface type to deal with the problem. Here is the code.

class PageServer{

  private Entry template;
  private JavaSpace _space;

  public PageServer(JavaSpace space){
    _space=space;

    Counter slowTemplate=new Counter("PageHits");
    template=_space.snapshot(slowTemplate);

    // other initializations
  }
  // the rest as before
}

The servePage() method remains unchanged. The JavaSpaces implementation that we were using made use of the snapshot hint, and our code to measure the number of hits became less intrusive. This is a good place to apply a snapshot because the template that we are using to match entries in the space does not change from one access to the next.

On our test code, we gained about a 15% improvement in the code block that performed the take, increment, write cycle that appears above. Obviously, this will vary with architectures, the physical location of servers, network bandwidth, etc., and the JavaSpace implementation.

The key point is that we have used snapshot to hint to the underlying space implementation that it should optimize the take operation. Now, the way that snapshot works is deliberately not defined in the JavaSpaces specification. So, before we dive into a discussion of what snapshot() is doing, it is important to understand that all space implementations may not use these techniques but may choose to use other techniques. It is also interesting to note that a specification-compliant implementation of snapshot may simply return the entry supplied to it as a parameter.

So how does the space achieve the performance improvement? In general, the space proxy shortcircuits some of the work of getting the entry ready to be sent to the space server. For example, the proxy can preserialize the entry and store the serialized form ready for transmission to the server.

The cost of this optimization is that the proxy keeps a copy of the entry in the client memory as well as constructing the new entry to be returned from the snapshot method. So there is a cost for the gain in performance, and you may wish to take this into account in some environments. For example, on a mobile handset, a client may make many snapshots to improve performance but as a result he may not have enough memory to keep the client application lively. Again, a good space proxy implementation will use a weak reference-based pool to allow the "pre-baked" entries to be dropped from the memory before more vital elements are reclaimed.

RULE

Snapshots are not guaranteed to work across JVM instances or space implementations.

It may also be possible for JavaSpaces implementations to use snapshot to make some optimizations in the space server. However, this may lead to partial failure problems, and specific implementations may treat this problem differently, and so a general discussion is not going to be applicable.

To finish, you should only use snapshot entries locally. If you make a snapshot in one space, it is not guaranteed to work with another space, and if you transport the snapshot to another JVM it is not guaranteed to work, even if you are using the same space.


Imitating snapshot

As we have already discussed, snapshot() stands alone as an optimizing call in the JavaSpaces interface. Maybe you have come across the argument that snap shot should be removed from the interface because it is not essential to the other operations that appear in the interface, for the very reason that its only function is optimization. In order to examine this, we suggest a quick but informative diversion to see what would happen if snapshot was to be removed from the JavaSpaces interface.

First, imagine that we have defined our own interface, jsip.Space for example (which is exactly the same as the standard JavaSpaces interface except that we have removed the snapshot method). We now implement a proxy that implements the jsip.Space interface, and this proxy then passes the call on to the standard JavaSpaces interface. You will have seen this type of architecture used in other proxy implementations throughout this book (see Figure 14.1).

Figure 14.1. Trying to imitate snapshot

The job of the optimizing proxy is to guess where to make a snapshot of an entry by automatically spotting the usage pattern, which we have already seen. Where the proxy is repeatedly called with an entry that does not change, this is a candidate to be "snapshotted". In other words, we have taken on the task of performing optimization within the space implementation without using snapshot hints from the client.

All of the possible implementations of such a proxy that we can think of greatly increase memory usage and processing overheads in the various method calls to the space. In fact, we have not been able to come up with an implementation that can make any significant impact on the performance of the overall system without consuming a disproportionately large amount of memory and processor time.

You may like to try this as either a mental exercise or actually write the code. If you can come up with an implementation that can improve performance without burning memory and slowing general use of the other API calls, we would be very interested in seeing it.

For now, therefore, snapshot stays a very useful and efficient addition to the JavaSpaces interface.


General entry optimizations

We have already looked at one space-based optimization in Chapter 2 – the payload idiom. In this section, we are going to use this idiom and apply some additional tuning techniques.

TIP

Use serialization optimizing techniques to improve the overall performance of the serialization of an entry's fields.

We know that entries have their fields serialized independently of the entry class itself, and we can apply some basic serialization optimizations to an entry's fields where necessary. Given the following entry class:

public class TestEntry
  implements net.jini.core.entry.Entry{

  public String _id;
  public Payload _payload;
}

we are going to apply the following changes to its payload field:

public class Payload
  implements java.io.Serializable {

  public String _name;
  public int _height;
  public String _destination;
}

First, always define a serialVersionUID within the class, as this stops the serialization system from dynamically computing the serialVersionUID when the object is serialized. Use the serialver tool to give you the value for the field rather than generating and administering your own numbering system. Second, you can write your own writeObject() and readObject() methods. This can offer considerable advantages, because the default mechanism has to expend a lot of effort in working out the structure of the objects by using introspection before flattening out the field contents.

As an alternative to this, we can make the class Externalizable instead of Serializable and implement the readExternal() and writeExternal() methods, which we have chosen to do with the Payload class along with a hard-wired serialVersionUID.

import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Payload implements Externalizable {

  static final long serialVersionUID
                     = 97356293052749534L;

  public String _name;
  public int _height;
  public String _destination;
  public void writeExternal(ObjectOutput oo) {
    try {
      oo.writeObject(_name);
      oo.writeInt(_height);
      oo.writeObject(_destination);
      }
    catch (Exception exp) { exp.printStackTrace(); }
}
    public void readExternal(ObjectInput oi) {
      try {
        _name = (String) oi.readObject();
        _height = oi.readInt();
        _destination = (String) oi.readObject();
        }
      catch (Exception exp) { exp.printStackTrace(); }
  }

}

Because our example is relatively simple you should not expect too much improvement in overall performance but, as the number of fields and the complexity of the objects increases, the more advantage can be gained. You can apply these optimizations at any level in the object hierarchy, and applying them through the whole object graph should greatly improve performance.


Date matching

In Chapter 3 in this book we used String fields to hold a date/time value, not because we intended to match on them, but because we were interested in the host local date/time value.

Some space-based systems need to match entries based on a date, for instance. If our space was holding exchange rate values leased for, say, a week, we might want to read what the rate was for a particular currency on a specific date. Simply creating a Date or Calendar object and assigning it to a field in an entry isn't a good option unless all your clients are running in the same time zone, because matching will fail. The reason for this is the mechanism by which Date and Calendar objects are serialized – under the hood they get serialized as a long, which includes time zone offsets. This means if a Date or Calendar was written to the space from a client in one time zone and the same values were used in a template in a client in a different time zone, then the match would fail due to the differences in the underlying serialized form.

To get around this, you need to agree a protocol that all clients must adhere to. In the case of Date or Calendar objects, you need to set the time zone to UTC (Universal Time Coordinated) as follows.

entry.cal.setTimeZone(TimeZone.getTimeZone("UTC"));

An alternative is to agree on a String format you're going to use for dates, for example DD-MM-YY.

TIP

If efficiency is a concern, use a String rather than a Date or a Date rather than a Calendar to reduce the processing required for the serialized field.

An interesting side-effect of using String for a date field is it yields a much smaller byte array when serialized than either a Date or Calendar object. It's also worth mentioning that a Date produces a smaller byte array than a Calendar object.

You will also have noticed that we sometimes directly gain the system's time (UTC) from the system clock as a long value using System.currentTimeMillis() and then wrap the returned long in a java.lang.Long for storage in an entry. If you are using this level of time measurement, you may want to make sure that your systems' clocks are kept in synchronization very accurately.


Time and spaces

Spaces use techniques such as switchable absolute/relative leases that must be used and as such mask the complexities involved in programming under this model.

Time does matter, but spaces make us take accout of it without it being obvious that this is what we are doing. This hides the issue from the developer.

For example, when a client receives a lease from JavaSpace the communication is based around a duration (net.jini.core.Lease.DURATION) in milliseconds, rather than a common understanding of the absolute time on both or all machines in the system. It is assumed that all the machines in the system have clocks that pass time relatively accurately, so that clock drift can be disregarded.

You can write Jini services that use an absolute form of leasing (net.jini. core.Lease.ABSOLUTE ) which makes the assumption that the clocks on system components are "sufficiently synchronized".

In our logging system in Chapter 13 we also record the absolute time that a call is made from within the client using the local time on that machine. It is also possible to construct space-based protocols that make use of machine local time. So there are many instances where making sure that all the machines within a space-based system are synchronized is vital to the operation of the system.

If you are setting up a system to synchronize system clocks, such as a UNIX timed/timeslave system, or the more global Network Time Protocol (NTP) system, we suggest that you locate your "reference" time server at the core of your system beside the space server.

For a local time-based system, a hub topology works very well, as shown in Figure 14.2.

Figure 14.2. Hub topology

For a system that needs time to synch globally, it is possible to use a network time protocol-based system with a physical network time server if possible. Try to locate the time server "near" the key space servers. In systems where "pedal to metal" performance is vital, a separate physical time server will reduce the load on the space server and hence increase total object throughput.

Below is a possible physical configuration running two space servers, a physical NTP server and some space clients through a local router. The NTP server is equidistant to all the other elements in the system via the use of the router. The router is used to automatically route both the space-based network traffic and the "icmp" packets used to coordinate the system clocks. Assuming that the clocks within the system don't drift too badly, it is possible to turn down the number of "icmp" packets that are being sent on the network, to leave more network bandwidth for space-related communication. This is illustrated in Figure 14.3.

Figure 14.3. NTP server configuration
    I l@ve RuBoard


Server tuning

Thus far in this chapter we have been concerned with client-side tuning, but what about tuning the actual space itself? For the most part, this is down to the JavaSpace implementation you are using, as most commercial implementations will be configurable in one way or another. However, there are several things you can do for yourself independently of the configuration options available to you by writing your own JavaSpace proxy adapter to wrap the "real" JavaSpace proxy.

In this section we are going to take a look at one useful implementation of a wrapped proxy that we are going to use to restrict the number of concurrent hits on the space – to implement this we are going to use a distributed semaphore.

In general terms, semaphores are used to limit the number of concurrent accesses to a resource. When the number of concurrent accesses has reached a predefined limit, all other threads/processes must wait until one or more processes using the semaphore completes and exits. You can think of a semaphore like a fixed-size resource pool; that is, once all the resources are in use everybody else has to wait until one becomes available.

In JavaSpaces, semaphores are best implemented as n entries in the space, where each client that needs access to a resource (protected by the semaphore) takes a semaphore (under a transaction), does what it needs to and then writes the semaphore entry back to the space.

Continuing the resource pool analogy, let's assume that we have written a space-based system and we want to limit the number of concurrent users. This is to avoid "thrashing" the space. An example of this could be when a space is being used as a back-end to a web service, which could receive hundreds of hits per second at certain times of the day. As there may be more than one instance of the web service running on the network, we can't limit the number of concurrent hits on the space from a centralized process, so it makes sense to use the space itself to handle this.

The first thing we need is an entry to represent the semaphore. We can write this to the space n times, where n represents the maximum number of concurrent hits on the space we want to permit – this can always be changed at runtime by an "administrator" service, which can increase or decrease the number of semaphores in the space.

public class Semaphore
   implements Entry{
public String _id;
}

Next we need a wrapper class for the web service to use that hides the fact we're limiting the number of hits on the space. A neat way to do this would be to write a Jini proxy, which delegates to the space proxy only when a semaphore can be taken. We look at how to write adapter and smart proxies in Chapters 9 and 13, but to summarize how this is done we register a separate proxy (with the Jini Lookup Service), which implements the JavaSpace interface and contains a reference to a real JavaSpace proxy.

The write method of the adapter proxy would look something like the code below, where _delegateSpace is the proxy to the "real" space.

public Lease write(Entry entry,
       Transaction txn,long lease)
  throws net.jini.core.transaction.TransactionException,
       java.rmi.RemoteException {

  Transaction semaTxn=TxnManager.create(TXN_LEN);
  //take a semaphore
  Entry semaphore=
   _delegateSpace.take(semaTmpl,semaTxn,WAIT_TIME);

  //delegate to the real space
  Lease result=_delegateSpace.write(entry,txn,lease);

  //put the semaphore back
  space.write(semaphore,semaTxn,LEASE_LEN);
  //commit the semaphore's txn
  semaTxn.commit();
 //return the lease
  return result;
}

By the careful configuration and use of Jini Lookup Services and subnet boundaries, we can hide the real space from the web service, only allowing it to discover the Adapter Proxy.

An alternative and much easier approach is to write the web service to perform its lookup based on the wrapped proxy's interface (or marker interface) or on a set of service attributes that are unique to the wrapped proxy. The neat thing about this technique is that the web service doesn't have to have any specialized code in it – it simply uses the JavaSpace interface and is unaware that its access is being restricted. For this technique to work, a transaction must be used otherwise semaphore entries could be lost.

Finally, it's worth pointing out that although this technique will slow down access to the space, if used in combination with lazy updates it can result in some interesting practical applications when you need to restrict the number of concurrent hits on a space.


Summary

In this chapter, we have taken a look at many different types of tuning techniques. The key rule is to apply tuning once a system has been made to work, and not before. This is not to say that you should not design with performance in mind, but don't let performance aspects control your design.

Of course, you must use logging and measurement tools, however crude or sophisticated they are, as part of the tuning process or you won't have any way of measuring what, if any, improvements you have made to the system.

Tunings can be applied at the local JVM level and there are many books and articles that can help you; see, for example, Shirazi (2000) or Bulka (2000). Tuning can also be applied at the JavaSpaces interface level using snapshots and more efficient space usage idioms; systems can also be tuned by changing architectures, moving clients "nearer" to a server, or on to more appropriate hardware, for example.

In some systems, synchronizing system clocks is essential to make sure that all of the system's components share (as much as possible) their "understanding" of time. By using adapter proxies, it is possible to add an additional level of control over how the JavaSpace is used.

For general Java tuning techniques, you may want to take a look at Dov Bulka's Java Performance and Scalability. There are also many other RMI and serialization tunings that can be applied. We suggest that you take a look at Java.rmi: The Remote Method Invocation Guide Book, by Esmond Pitt and Kathleen McNiff for a whole host of these.

For more information on absolute and duration leases, see the Jini Technology Core Platform Specification, which is available along with the Jini download from Sun.


Conclusion

One of the main aims of this book was to increase your understanding of where JavaSpaces are applicable and how to use spaces effectively within your system's architectures. We hope we have done this; now it is over to you!

Please let us know, via the book's website, if you found this information useful, and if it prompted any changes to the space-based designs in your own application area or solution domain.

We would be particularly interested in any further designs or space-based patterns that you discover and reuse while building systems. As ever, the book's website is your first port of call for further information and contact details.


Appendix A. Code listings

This appendix contains the code listings for the utility classes that we have used throughout this book. Some classes that we have not listed in full are also given in their complete form.

You can find the source code given here, along with other utility and example code, on the book's website.


ServiceLocator

package jsip.jini;

import net.jini.core.entry.*;
import net.jini.lookup.entry.Name;
import net.jini.space.*;
import net.jini.lookup.*;
import net.jini.discovery.*;
import net.jini.core.lookup.*;
import java.rmi.*;

import jsip.debug.Debug;

public class ServiceLocator{
    private Object _proxy;
    private Object _lock=new Object();
    private ServiceTemplate _template;

    public static Object getService(Class serviceClass)
       throws java.io.IOException,InterruptedException{

       return getService(serviceClass,null);
   }
   public static Object getService(Class serviceClass,
                                  String serviceName)
       throws java.io.IOException,InterruptedException{
     ServiceLocator sl=new ServiceLocator();
     return sl.getServiceImpl(serviceClass,serviceName);
   }

   private Object getServiceImpl(Class serviceClass,
                               String serviceName)
     throws java.io.IOException,InterruptedException{

      Class [] types=new Class[]{serviceClass};
      Entry [] entry=null;

      if(serviceName!=null){
        entry=new Entry[]{new Name(serviceName)};
      }

      _template=new ServiceTemplate(null,types,entry);

      if (System.getSecurityManager() == null) {
          System.setSecurityManager(new
          RMISecurityManager());
      }

        LookupDiscovery disco=
          new LookupDiscovery(new String[] { "" });

        disco.addDiscoveryListener(new Listener());

        while(_proxy==null){
            synchronized(_lock){
               _lock.wait();
          }
        }
      disco.terminate();
      return _proxy;

    }
    class Listener implements DiscoveryListener {
       public void discovered(DiscoveryEvent ev) {

          Debug.message(ev);

          ServiceRegistrar[] reg = ev.getRegistrars();
          for (int i=0 ; i<reg.length && _proxy==null ; i++) {
         findService(reg[i]);
       }
    }
    public void discarded(DiscoveryEvent ev) {
    }
  }
  private void findService(ServiceRegistrar lus) {

    try {
       synchronized(_lock){

          _proxy=lus.lookup(_template);

          if(_proxy!=null){
             Debug.message("Proxy ="
                            +_proxy.getClass());

             _lock.notifyAll();
          }
       }
    }catch(RemoteException ex) {
        Debug.message(ex.getMessage());
    }

  }
}


SpaceLocator

package jsip.jini;

import net.jini.space.JavaSpace;

public class SpaceLocator{

   public static JavaSpace getSpace()
       throws Exception{

       return (JavaSpace)
          ServiceLocator.getService(JavaSpace.class);

   }
   public static JavaSpace getSpace(String spaceName)
      throws Exception{

      return (JavaSpace)
            ServiceLocator.getService (JavaSpace.class,
            spaceName);

   }
}


TxnManager

import net.jini.core.lease.*;

public class TxnManager{

    private static TransactionManager _impl;

    public static Transaction createTxn(long lease)
      throws LeaseDeniedException,
             Exception{

      if(_impl==null){
          getTxnMgr();
      }
      Transaction.Created txnC
        =TransactionFactory.create(_impl,lease);
      return txnC.transaction;

      }
      public synchronized static TransactionManager
      getTxnMgr()
        throws Exception{

        if(_impl==null){
            Debug.message("Locating a TransactionManager
            ...");

            _impl=(TransactionManager)
              ServiceLocator.getService(
                 TransactionManager.class);

            Debug.message("TransactionManager found!");
     }
     return _impl;
   }
}


Debug

package jsip.debug;

public final class Debug
{
   private static boolean debugOn;

   static{
      //from System property
      //e.g -Djip.debug=true
      debugOn=Boolean.getBoolean("jsip.debug");
   }
   public synchronized static void message(Object obj){
       if(debugOn){
          StringBuffer buf=new StringBuffer();
          buf.append("jsip debug: ");
          buf.append(obj);
          System.out.println(buf.toString());
       }
   }
}


ArrayMetaData

/**
* ArrayMetaData is used by ExpandableArray
*
*/

package jsip.array;

public class ArrayMetaData
    implements net.jini.core.entry.Entry{

    public String _name;
    public Integer _index;
    public Integer _type;
    public static final Integer HEAD=new Integer(0);
    public static final Integer TAIL=new Integer(1);

    //required no-args constructor
    public ArrayMetaData(){

    }
    //for matching
    public ArrayMetaData(String name,Integer type){
       _name=name;
       checkType(type);
       _type=type;
    }
    //for creation
    public ArrayMetaData(String name,Integer type,int index){
       this(name,type);
       _index=new Integer(index);
    }
    //increment
    public void incrementIndex(){
       _index=
       new Integer(_index.intValue()+1);
    }
    private void checkType(Integer type){
        if(type.equals(HEAD)==false
           && type.equals(TAIL)==false){

           throw new IllegalArgumentException("Invalid type");
        }
    }
}


ExpandableArray

package jsip.array;

import net.jini.space.*;
import net.jini.core.transaction.*;
import net.jini.core.transaction.server.*;
import net.jini.core.lease.*;
import java.rmi.RemoteException;
import java.util.Iterator;
public class ExpandableArray{

   private JavaSpace _space;
   private TransactionManager _txnMgr;
   private String _name;

   /**
   * Simple constuctor for testing without Txn mgr
   */

   public ExpandableArray(String name,JavaSpace space){
      this(name,space,null);
   }
   /**
   * Robust constuctor for testing with Txn mgr
   */

   public ExpandableArray(String name,
           JavaSpace space,TransactionManager txnMgr){
      _name=name;
      _space=space;
      _txnMgr=txnMgr;
   }
   /**
   * Create the array
   */

   public void create()
      throws RemoteException{

      try{
         Transaction txn=createTxn();

         ArrayMetaData head=
            new ArrayMetaData(_name,ArrayMetaData.HEAD,0);
         ArrayMetaData tail=
            new ArrayMetaData(_name,ArrayMetaData.TAIL,0);

      //assume lease is granted
      //for correctness use a lease renewal manager
      _space.write(head,txn,Lease.FOREVER);
      _space.write(tail,txn,Lease.FOREVER);

          if(txn!=null){
            txn.commit();
         }
         jsip.debug.Debug.message(_name+" created");

      }catch(Exception ex){
           throw new RemoteException("create failed",ex);
      }
   }
   /**
   * Delete the array and all elements
   */
   public void delete()
       throws RemoteException{

       try{
         jsip.debug.Debug.message(_name+" delete");

         Transaction txn=createTxn();

         ArrayMetaData head=
            new ArrayMetaData(_name,ArrayMetaData.HEAD);
         ArrayMetaData tail=
            new ArrayMetaData(_name,ArrayMetaData.TAIL);

         _space.take(head,txn,500);
         _space.take(tail,txn,500);

         ArrayElement tmpl=new ArrayElement(_name);
         //debug
         int remCount=0;
         while(_space.take(tmpl,txn,500)!=null){
            remCount++;
         }

         jsip.debug.Debug.message(
           _name+" "+remCount+" elements removed");

         if(txn!=null){
            txn.commit();
         }
         jsip.debug.Debug.message(_name+" delete - DONE");

      }catch(Exception ex){
         throw new RemoteException("create failed",ex);
      }
   }
   /**
   * Append an element to the array
   */
   public void add(java.io.Serializable data)
      throws RemoteException{

      try{
         Transaction txn=createTxn();

         ArrayMetaData tmpl=
            new ArrayMetaData(_name,ArrayMetaData.TAIL);

         ArrayMetaData tail=
            (ArrayMetaData)_space.take(tmpl,txn,500);

         while(tail==null){
            tail=(ArrayMetaData)_space.take(tmpl,txn,500);
         }
         Integer index=tail.getIndex();
         tail.increment();
         ArrayElement e=new ArrayElement(_name,index,data);

         //assume lease is granted
         //for correctness use a lease renewal manager
         _space.write(e,txn,Lease.FOREVER);
         _space.write(tail,txn,Lease.FOREVER);

         if(txn!=null){
            txn.commit();
         }
         jsip.debug.Debug.message(_name+" add "+data);

      }catch(Exception ex){
         throw new RemoteException("add failed",ex);
      }
   }
   /**
   * Return an iterator over the array
   * @param timeout max time to wait for next entry
   */
   public Iterator iterator(long timeout)
       throws RemoteException{
   try{

      ArrayMetaData tmpl=
         new ArrayMetaData(_name,ArrayMetaData.HEAD);
      ArrayMetaData head=
         (ArrayMetaData)_space.read(tmpl,null,500);

      while(head==null){
         head=(ArrayMetaData)_space.read(tmpl,null,500);
      }
      Integer startIndex=head.getIndex();
      return new IteratorImpl(timeout,startIndex);

   }catch(Exception ex){
      throw new RemoteException("failed to create iterator",
      ex);
   }

}
private class IteratorImpl
   implements Iterator{

private long _timeout;
private Integer _index;
private Object _next;

public IteratorImpl(long timeout,Integer index){

   _timeout=timeout;
   _index=index;
}

public boolean hasNext(){
  try{

    ArrayElement tmpl=new ArrayElement(_name,_index);
       _index=new Integer(_index.intValue()+1);

       _next=_space.read(tmpl,null,_timeout);

       return _next!=null;
    }catch(Exception ex){

       return false;
    }
   }
   public Object next(){
      return ((ArrayElement) _next)._data;
   }
   public void remove() {
      throw new UnsupportedOperationException();
   }

}
private Transaction createTxn()
    throws LeaseDeniedException, RemoteException{

    if(_txnMgr==null){
        return null;
    }
   //here we pick a default timeout of 1 minute
   //this could be a parameter or field
   Transaction.Created txnC=
     TransactionFactory.create(_txnMgr,60000);
   return txnC.transaction;

   }
}


SpaceSafeConstant

/*
* Example usage
* public class MyConsts extends SpaceSafeConstant{
* public final MyConsts ONE=new MyConsts();
* public final MyConsts TWO=new MyConsts();
* private MyConsts(){}
* }
*/

package jsip.util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public abstract class SpaceSafeConstant
    implements java.io.Serializable{
   //the internal representation of the constant
   private transient String _fieldName;

   /**
   * get the field name for this instance
   * write it to the stream
   */
   private void writeObject(java.io.ObjectOutputStream out)
        throws java.io.IOException {

     Class clazz=getClass();
     Field [] f=clazz.getDeclaredFields();

   for(int i=0;i<f.length;i++){
      try{
              int mod=f[i].getModifiers();

              if(Modifier.isStatic(mod) && Modifier.isFinal
              (mod)
                    && Modifier.isPublic(mod)){

      if(this==f[i].get(null)){
               String fName=f[i].getName();
               out.writeObject(fName);
      }
        }
   }catch(IllegalAccessException ex){
       throw new java.io.IOException(ex.getMessage());
   }
  }
}

/**
* read the serialized field name and assign to
* _fieldName
*/
private void readObject(java.io.ObjectInputStream in)
   throws java.io.IOException{
  try{

          _fieldName=(String)in.readObject();
      }catch(ClassNotFoundException ex){
          throw new java.io.IOException(ex.getMessage());
      }
   }
/**
 * replace the deserialized instance with the
 * local static instance to allow correct
 * usage of == operator
 */
public Object readResolve()
    throws java.io.ObjectStreamException{

    try{

        Class clazz=getClass();
        Field f=clazz.getField(_fieldName);
        return f.get(null);
    }catch(Exception ex){
       ex.printStackTrace();
       throw new java.io.InvalidObjectException(
              "Failed to resolve object");
    }

  }
}


Queue

package jsip.util;

//guarded queue
public class Queue{

   private java.util.List _queue
           =new java.util.ArrayList();

   public void add(Object newEntry){
       synchronized(_queue){
         _queue.add(newEntry);
         _queue.notifyAll();
      }
   }
   public Object getNext()
      throws InterruptedException {
      synchronized(_queue){
         while(_queue.isEmpty()){
               _queue.wait();
            }
            return _queue.remove(0);
      }
   }
}


QueuedEventListener

package jsip.notify;

import java.rmi.server.UnicastRemoteObject;
import net.jini.core.event.*;

public abstract class QueuedEventListener
           extends UnicastRemoteObject
   implements RemoteEventListener, Runnable{

   private Queue _queue=new Queue();

   public QueuedEventListener()
      throws java.rmi.RemoteException{
         super();
         Thread t=new Thread(this);
         t.start();
   }
   public void notify(RemoteEvent theEvent)
      throws UnknownEventException,
             java.rmi.RemoteException{

      _queue.add(theEvent);
   }
   //subclasses do the actual processing at their leisure
   public abstract void processEvent(RemoteEvent evt);

   public void run(){
      while(!Thread.currentThread().isInterrupted()){
        try{
           processEvent( RemoteEvent) _queue.getNext());
        }catch(InterruptedException ex){
           return;
        }
      }
   }
}


Appendix B. Tips and rules in this book

Chapter 1

Rule: JavaSpaces contain and distribute Java objects that implement the entry interface at the top level.

Rule: JavaSpaces associate entries.

Rule: JavaSpaces are distributed.

Tip: Always use a transaction with a take /write sequence.

Rule: The JavaSpaces implementation is free to return a lease that is shorter than the requested lease, but not longer.

Tip: Use the Lease Renewal Manager or Lease Renewal Service to renew leases if the requested lease time is not granted by the space.

Rule: When a transaction is passed to the read method the JavaSpace can return a matching entry written within the scope of that transaction, or one that is already visible.

Rule: If a matching entry is returned from read invoked with a transaction parameter, no other transaction can take that entry until the transaction either commits or aborts.

Chapter 2

Tip: If an object implements Entry (directly or indirectly) you should read public access of fields to mean "matchable and storeable in a space".

Rule: All entries must implicitly or explicitly have a public no-args constructor.

Tip: Define both default and "populating" constructors for all entry classes.

Tip: Use the payload idiom to reduce the indexing burden on the space.

Tip: Consider carefully which fields are candidates for matching before using the payload idiom.

Chapter 3

Tip: Consider moving fields from nested classes to top-level entries.

Tip: Always use a transaction in conjunction with take operations.

Chapter 4

Tip: Use polling with read and take operations.

Tip: Declare generic (object) fields in entries as Serializable .

Chapter 7

Tip: Return from notify() as quickly as possible.

Tip: Never assume that the space will grant you the lease length you request.

Tip: Never ignore the EventRegistration object returned by notify() .

Tip: Always renew leases until you have finished with a resource.

Chapter 12

Tip: Always consider a sleeping polling access pattern when immediate notification is not an overriding factor.

Rule: Notify is only as timely as your space implementation (and network) will allow – the specification does not require a minimum response time.

Chapter 13

Tip: Build and test a small system first to test out your assumptions.

Chapter 14

Rule: Tune a system once it is working – never before!

Tip: Analyze your tuning effectiveness by measuring the total object throughput at a system-wide level.

Tip: Make the time between the take and the write of an entry as short as possible.

Tip: Using lazy updates can boost performance.

Rule: The type of template returned from a call to snapshot is not defined and should not be assumed.

Rule: Snapshots are not guaranteed to work across JVM instances or space implementations.

Tip: Use serialization optimizing techniques to improve the overall performance of the serialization of an entry's fields.

Tip: If efficiency is a concern, use a String rather than a Date or a Date rather than a Calendar to reduce the processing required for the serialized field.


I l@ve RuBoard Previous Section

References

Dov Bulka (2000) Java™ Performance and Scalability, Volume 1: Server-side Programming Techniques. Addison-Wesley: Reading, MA.

W. Keith Edwards (2001) Core Jini™, second edition. Prentice-Hall: Hemel Hempstead, UK.

James Foley, Andries van Dam, Steven Feiner and John Hughes (1995) Computer Graphics Principles and Practice, second edition in C. The Systems Programming Series, Addison-Wesley: Reading, MA.

Eric Freeman, Suzanne Hupfer and Ken Arnold (1999) Javaspaces™ Principles, Patterns, and Practice. The Jini™ Technology Series, Addison-Wesley: Reading, MA.

David Gelernter (1985) Generative communication in Linda. ACM Transactionson Programming Languages and Systems 7(1): 80–112.

Jini Entry Specification (part of the Jini™ Technology Core Platform Specification). Available from Sun Microsystems.

Esmond Pitt and Kathleen McNiff (2001) Java™.rmi: The Remote Method Invocation Guide. Addison-Wesley: Reading, MA.

Jack Shirazi (2000) Java™ Performance Tuning. O'Reilly: Farnham, UK.

W. Richard Stevens and Gary R. Wright (1995) TCP/IP Illustrated, Volume 2: The Implementation. Addison-Wesley: Reading, MA.

Nigel Warren and Phil Bishop (1999) Java™ in Practice: Design styles and idioms for effective Java. Addison-Wesley: London.
    I l@ve RuBoard Previous Section