Project developing progress:
Refactored a lot, refined some, so far actors work fine with good performance. Now I'm working on next version.
- [x] Most coding work.
- [x] Javadoc.
- [x] Actor simulation test.
- [x] Refining and test.
- [x] Simple usage description.
- [x] Detailed document.
- [x] Performance description.
- [x] Release.
- [ ] More necessary description and additional doc.
- [ ] Next version.
What is Psmm?
Psmm is the abbreviation for PSeudo-Mutable Message. It's a java library for actors to easily create and access immutable messages as if they were mutable ones, and with nice performance.
Messages are passed on from actors to actors, thus probably shared by threads. Akka document clearly states that messages must be immutable, otherwise Akka actors can't ensure eliminating evils of low level java concurrency. But for writing immutable object, you need to be careful about things. In this note, I wrote Psmm, so we can easily generate immutable message.
Getting started:
Maven dependency:
<dependency>
<groupId>com.github.cuzfrog</groupId>
<artifactId>psmm</artifactId>
<version>1.0.0</version>
</dependency>
First needing to initiate PsmmSystem:
PsmmSystem.initiate(); //only invoke once in your main thread
Creating a new message
UMessage message =
Messages.builder() //get builder
.set(key1, value1)
.set(key2, value2) //add some data
...
.build();
Modifying an existed one:
UMessage messageAfter =
messageBefore
.set(key1, value1)
.set(key2, value2) //add or reset some data
...
.build();
Regressing a message:
UMessage message3 =message2.regress();
Regression is very useful when actors work in pipeline, e.g. actor can simply check and regress a message to redo rather than resend one.
Code example:
import com.github.cuzfrog.psmm.Messages;
import com.github.cuzfrog.psmm.PsmmSystem;
import com.github.cuzfrog.psmm.UMessage;
class SimpleTest {
@SuppressWarnings("boxing")
public static void main(String[] args) {
// TODO Auto-generated method stub
PsmmSystem.initiate(); //only invoke once in your main thread
//create a new message:
String key1 = "int1";
String key2 = "int2";
String key3 = "str1";
UMessage message1 =
Messages.builder() //get builder
.set(key1, 2)
.set(key2, 122)
.set(key3, "this is string") //add some data
.build(); //without build() compile time error.
//after passed to another actor, fetch data:
System.out.println(message1.get(key1)
+ "|" + message1.get(key2)
+ "|" + message1.get(key3));
//result: 2|122|this is string
String key4 = "double1";
//"modify" last message, reset some data, add some new data:
UMessage message2 =
message1
.set(key2, 45)
.set(key3, "this is another string")
.set(key4, 568.3d)
.build();
//after passed to another actor, fetch data:
System.out.println(message2.get(key1)
+ "|" + message2.get(key2)
+ "|" + message2.get(key3)
+ "|" + message2.get(key4));
//result: 2|45|this is another string|568.3
UMessage message3 =message2.regress();
if(message3!=null){
System.out.println(message3.get(key1)
+ "|" + message3.get(key2)
+ "|" + message3.get(key3)
+ "|" + message3.get(key4));
}
//result: 2|122|this is string|null
}
}
Using typed message (others are the same):
TBuilder<String, Integer> b2 = Messages.typedBuilder();
TMessage<String, Integer> message2 = b2.set("key1", 583)
// .set("key2", "string value") //compile time error
.build();
NOTE:When use typed message, you must ensure type's immutability.
Anti-patterns: Be careful!
[Java puzzles-56]
//now you received a message.
message.set("key1", 583).set("key2", 1773).build(); //you think you updated it.
//then send the message. ERROR, the message you sent is not the new one
//right version - must give a new reference:
UMessage newMessage=message.set("key1", 583).set("key2", 1773).build();
//or use the old reference:
message=message.set("key1", 583).set("key2", 1773).build();
Document
Introduction
Psmm messages take advantage of Decorator Pattern. Every message has a reference of preceded message. For newly created message, PsmmFactory puts a RootMessage in it. After a message has been set("modified"), factory create a new message decorating the old one, of course, with new data. Therefore, you can access psmm as if it were mutable.
For class info, please see source file.
Styles
Psmm includes different *Styles of messages standing for different structures.
A1. Linked(default): decorate old message when every setting. When getting data, first check keys in newest message, then the last one. This is the fast way to create new object, because when you create it, you don't care about the old data.But when the chain is long, it'll be slower to fetch some data. It also provides a way to regress a message: shell the outermost message, return the inner one.
A2. Flat: no decoration, read data from old message and merge them into new message.
B1. Typed: need to explicitly indicate Generic parameter Type. Thus gives flexibility to put user defined data object into message.
B2. Untyped: only takes basic immutable object, such as String, Integer etc. When retrieving data, need to cast.
C. Retained(deprecated): references of messages created are stored in a pool. Why not reuse immutable object? (But there might be a pitfall, is it really necessary to retain small object in modern JVM? Generally speaking, this is a bad idea and likely to cause other undesirable potential problems. For now I deprecated this feature. All message will be newly created.)
D1. Value: One message equals another depending on their values that they exhibit.
D2. Unique (default): No override to equals() method, message only equals itself.
The word *Style is used for distinguishing from Parameter Type.
How to choose style:
Messages.builder(Messages.Style.FLAT_MAP)
Builder and Factory model
- Builder(Originally called raw message) is a concept helper class which is exposed to user instead of a factory, and to ensure message is actually created. It's a Proxy of a factory. Consider situation below:
UMessage message2 =
message1
.set(key2, 45) //when first call set(), it turns to a raw message
.set(key3, "this is another string")
.set(key4, 568.3d)
//.build(); //no message is really created. Type mismatch, compile time error.
A builder cannot evaluate to UMessage. Also, in concept, it's better than return a factory object.
- Factory do the real job to store user data and create message. It utilizes the Builder Pattern. Factory has a final reference of raw message, which means they're paired when their instance are created. The reason to do so is because only factories are store in Factory Pool.
Factory detail
- Module is factory's component class, doing concrete job inside factory. In this Strategy Pattern, same factory can be used to create different styles of message. Modules are thread safe or state less, so several factories are able to reuse same Module instances. Modules are created once when PsmmSystem initiates and stored into an EnumMap. When needed, they are dynamically assembled into factory that are calling them. If a factory is about to create a second message with the same Style as the last one, module inside will not change which avoids module-map re-searching. Hence better performance with consistent Style throughout your application.
There are three kinds of Modules:
Creation: creates concrete message, like ValueMessage or UniqueMessage.
Data: designates Data implementation into factory, like HashMap.
Structure: decides which style, like Flat or Linked.
Modules work in combine, taking advantage of Decorator Pattern.
Factory Pool stores created factories in it, which implements a ThreadLocal to attach a new factory instance to every thread. When fetching factory, module is assembled into factory according to message's style.
Data class to encapsulate concrete data-store strategy. For now, there's only MapData that implements HashMap. Factory has a Data inside, when create message, it give that Data to message and ditch the Data's reference.
PsmmSystem and helper Messages
- PsmmSystem holds instances of factory pool and initiates Modules, internally providing static methods. Instantiating Module at runtime gives more flexibility.
- Messages provides factory methods to create message.
Configuration
Psmm provides a class PsmmConfiguration to do some custom setup when initiation. Later I shall probably provide way to customize factory Module and Data through configuration.
How to setup:
PsmmConfiguration config=new PsmmConfiguration()
.setFactoryPoolChoseType(FactoryPoolType.NULL) //no factory pooling
...
PsmmSystem.initiate(config);
Performance
I've prepared some performance test data, and I'll upload them, and explain in detail.
1. creating new message and set one pair of data with factory pooled:(20 rounds warm up and tests)
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
testCreateControlMessage | avgt | 20 | 237.161 | ± 0.889 | ns/op |
testCreateTypedFlat | avgt | 20 | 174.108 | ± 3.171 | ns/op |
testCreateTypedLinked | avgt | 20 | 179.215 | ± 3.695 | ns/op |
testCreateUntypedFlat | avgt | 20 | 175.835 | ± 2.523 | ns/op |
testCreateUntypedLinked | avgt | 20 | 172.466 | ± 1.766 | ns/op |
Tested with jmh: 10-20 warmups, 10-20 rounds, 8 threads. Tests simple create a new message and set a pair of random key and Integer. Control message(immutable) is created via two HashMaps, one as temporary data vehicle when creating, another as data field. HW: xeon E3 1230v2, 16G DDR3 1600. OS: Windows, Oracle JDK 1.8.0_51. See source file for more detail.
It's very important to know that generating message, in most cases, only takes a fraction of application time. Sometimes, it's really not necessary to worry about their performance. When I did micro-benchmark test, yeah, disparity could be huge. However, when I did actual actor interaction test, performances are in the same scale.
Javadoc
I have written complete javadoc.
Support or Contact
Author:Cause Chung (@cuzfrog)
Any problem, please hit me on GitHub or mail me: cuzfrog@139.com / cuzfrog@gamil.com
License
The MIT License (MIT)