├── .gitignore
├── cls
└── IAT
│ └── PubSub
│ ├── Example
│ ├── SubDummy.cls
│ ├── SubPatient.cls
│ ├── MsgOrder.cls
│ ├── MsgPatient.cls
│ ├── MsgDummy.cls
│ └── Main.cls
│ ├── Publisher.cls
│ ├── MsgBody.cls
│ ├── MsgHeader.cls
│ ├── Subscriber.cls
│ └── Manager.cls
├── LICENSE
├── inc
└── IAT.PubSub.INC
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bak
2 |
--------------------------------------------------------------------------------
/cls/IAT/PubSub/Example/SubDummy.cls:
--------------------------------------------------------------------------------
1 | /// Subscriber for Patient channel
2 | Class IAT.PubSub.Example.SubDummy Extends IAT.PubSub.Subscriber
3 | {
4 |
5 | /// Callback that is called when a message is received by subscriber
6 | Method OnMessage(pBody As IAT.PubSub.Example.MsgDummy) As %Status
7 | {
8 | set ret = $$$OK
9 | try {
10 | $$$LOG("MsgDummy. ID="_pBody.%Id()_" Text="_pBody.Text)
11 | } catch ex {
12 | set ret = ex.AsStatus()
13 | }
14 | quit ret
15 | }
16 |
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/cls/IAT/PubSub/Example/SubPatient.cls:
--------------------------------------------------------------------------------
1 | /// Subscriber for Patient channel
2 | Class IAT.PubSub.Example.SubPatient Extends IAT.PubSub.Subscriber
3 | {
4 |
5 | /// Callback that is called when a message is received by subscriber
6 | Method OnMessage(pBody As IAT.PubSub.Example.MsgPatient) As %Status
7 | {
8 | set ret = $$$OK
9 | try {
10 | $$$LOG("MsgPatient. ID="_pBody.%Id()_" Name="_pBody.Name)
11 | } catch ex {
12 | set ret = ex.AsStatus()
13 | }
14 | quit ret
15 | }
16 |
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/cls/IAT/PubSub/Example/MsgOrder.cls:
--------------------------------------------------------------------------------
1 | Class IAT.PubSub.Example.MsgOrder Extends IAT.PubSub.MsgBody
2 | {
3 |
4 | Property Number As %String;
5 |
6 | Property Description As %String;
7 |
8 | Storage Default
9 | {
10 |
11 |
5 | /// ; create events and subscriber jobs 6 | /// do ##class(IAT.PubSub.Example.Main).Setup() 7 | /// ; send some messages to channels 8 | /// do ##class(IAT.PubSub.Example.Main).Run() 9 | /// ; show log 10 | /// zw ^PubSub.Log 11 | ///12 | Class IAT.PubSub.Example.Main Extends %RegisteredObject 13 | { 14 | 15 | Parameter ChannelPatient = "Patient"; 16 | 17 | Parameter ChannelDummy = "Dummy"; 18 | 19 | /// Config and setup Publisher-Subscriber 20 | ClassMethod Setup() As %Status 21 | { 22 | set ret = $$$OK 23 | try { 24 | set $$$Config(..#ChannelPatient, "Subscriber", "IAT.PubSub.Example.SubPatient") = 4 25 | set $$$Config(..#ChannelDummy, "Subscriber", "IAT.PubSub.Example.SubDummy") = 2 26 | $$$ThrowOnError(##class(IAT.PubSub.Manager).Setup()) 27 | } catch ex { 28 | set ret = ex.AsStatus() 29 | } 30 | quit ret 31 | } 32 | 33 | /// Delete messages 34 | ClassMethod Delete() As %Status 35 | { 36 | set ret = $$$OK 37 | try { 38 | $$$ThrowOnError(##class(IAT.PubSub.MsgBody).%DeleteExtent()) 39 | $$$ThrowOnError(##class(IAT.PubSub.MsgHeader).%DeleteExtent()) 40 | } catch ex { 41 | set ret = ex.AsStatus() 42 | } 43 | quit ret 44 | } 45 | 46 | /// Run tests. Send some messages channels. 47 | ClassMethod Run(pNum As %Integer = 5) As %Status 48 | { 49 | set ret = $$$OK 50 | try { 51 | $$$ThrowOnError(..SendPatient(pNum)) 52 | $$$ThrowOnError(..SendDummy(pNum)) 53 | } catch ex { 54 | set ret = ex.AsStatus() 55 | $$$LOG($system.Status.GetErrorText(ret)) 56 | } 57 | quit ret 58 | } 59 | 60 | /// Publish messages in patient channel 61 | ClassMethod SendPatient(pNum As %Integer = 5) As %Status 62 | { 63 | set ret = $$$OK 64 | try { 65 | for i=1:1:pNum { 66 | set data = ##class(IAT.PubSub.Example.MsgPatient).%New() 67 | set data.MRN = i 68 | set data.Name = i 69 | set data.LastName = i 70 | $$$ThrowOnError(data.%Save()) 71 | 72 | $$$LOG("Sending data "_i_" to "_..#ChannelPatient) 73 | $$$ThrowOnError(##class(IAT.PubSub.Publisher).Send(..#ChannelPatient, data)) 74 | } 75 | } catch ex { 76 | set ret = ex.AsStatus() 77 | } 78 | quit ret 79 | } 80 | 81 | /// Publish messages in dummy channel 82 | ClassMethod SendDummy(pNum As %Integer = 5) As %Status 83 | { 84 | set ret = $$$OK 85 | try { 86 | for i=1:1:pNum { 87 | set data = ##class(IAT.PubSub.Example.MsgDummy).%New() 88 | set data.Text = i 89 | $$$ThrowOnError(data.%Save()) 90 | 91 | $$$LOG("Sending data "_i_" to "_..#ChannelDummy) 92 | $$$ThrowOnError(##class(IAT.PubSub.Publisher).Send(..#ChannelDummy, data)) 93 | } 94 | } catch ex { 95 | set ret = ex.AsStatus() 96 | } 97 | quit ret 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Caché Publisher - Subscriber 2 | 3 | The purpose of this repository is to provide a simple example of *Publisher-Subscriber model* implemented using [InterSystems Caché](http://www.intersystems.com/our-products/cache/cache-overview/). 4 | 5 | This example came out of a session which discussed ways of processing work **asynchronously** using **persistent queues**. 6 | 7 | ## Installation 8 | ``` 9 | set path="C:\Temp\cache-iat-pubsub-master\cache" 10 | do $system.OBJ.ImportDir(path,"*.inc","ck",.error,1) 11 | do $system.OBJ.ImportDir(path,"*.xml","ck",.error,1) 12 | ``` 13 | 14 | ## Run the code 15 | ``` 16 | ; create events and subscriber jobs 17 | do ##class(IAT.S05.PubSub.Example.Main).Setup() 18 | ; send some messages to channels 19 | do ##class(IAT.S05.PubSub.Example.Main).Run() 20 | ; show log 21 | zwrite ^PubSub.Log 22 | ``` 23 | 24 | ## Implementation 25 | [Caché Event API](http://docs.intersystems.com/cache20152/csp/documatic/%25CSP.Documatic.cls?LIBRARY=samples&CLASSNAME=%25SYSTEM.Event&MEMBER=&CSPCHD=001000000000Np0VyNuBwTDT$PiD99hk51sfrKUAanvu8tIi0c&CSPSHARE=1) is used to let the subscriber processes sleep until they are notified. 26 | 27 | ### Main classes 28 | * Manager - handles core operations such as adding subscribers, terminate all subscriber jobs, etc. 29 | * Publisher - generic publisher, provides a basic *Send* operation which notifies a subscriber that a message is ready. 30 | * Subscriber - generic subscriber, all subscriber must extend from this. 31 |  32 | 33 | ### Example classes 34 | The following classes provides an example of how the main classes could be used, in this case there are two subscribers: 35 | * *SubPatient* which handles Patient related messages. 36 | * *SubDummy* which handles dummy messages. 37 |  38 | 39 | ### Log 40 | After running the example, the log stored in *^PubSub.Log* global will look similar to this: 41 |  42 | 43 | ### Further discussion 44 | There are several points of this example that can be discussed as an exercise: 45 | 46 | #### Caché Event API vs Semaphores 47 | * **$system.Event** (Caché Event API) can only wait / wake up resources on the **same system** (not networking supported). 48 | * It would be interesting implementing this same model using **$system.Semaphore** over an *ECP* scenario. 49 | 50 | #### Multiple types of subscribers 51 | * In *IAT.PubSub.Publisher:Send()* method, it simply creates a message header, save it to queue and send a signal to one of the subscribers. 52 | * It would be interesting allowing more than one type of subscriber per channel. In this case, the *Send()* method should create several message headers and signal the different types of configured subscribers. 53 | 54 | # Developer Community 55 | Have a look at [InterSystems Developer Community](https://community.intersystems.com/) to learn about InterSystems technology, sharing solutions and staying up-to-date on the latest developments. 56 | 57 | This example was published in https://community.intersystems.com/post/simple-systemevent-examples 58 | -------------------------------------------------------------------------------- /cls/IAT/PubSub/Manager.cls: -------------------------------------------------------------------------------- 1 | Include IAT.PubSub 2 | 3 | /// Publisher-Subscriber manager 4 | Class IAT.PubSub.Manager Extends %RegisteredObject 5 | { 6 | 7 | /// Setup Publisher-Subscriber system as specified in $$$Config global. 8 | /// This method can be called periodically to check that jobs are running as configured. 9 | /// Configuration global is expected to be like: 10 | /// $$$Config(CHANNEL, "Subscriber", SUBSCRIBERSUBCLASS) = NUMBEROFPROCESSES 11 | /// CHANNEL = channel name 12 | /// SUBSCRIBERSUBCLASS = full name of the subscriber class (must extend from Subscriber) 13 | /// NUMBEROFPROCESSES = number of processes that will be running subscriber class 14 | ClassMethod Setup() 15 | { 16 | set ret = $$$OK 17 | try { 18 | set channel="" 19 | for { 20 | set channel=$order($$$Config(channel)) 21 | quit:channel="" 22 | 23 | $$$ThrowOnError(..AddChannel(channel)) 24 | } 25 | } catch ex { 26 | set ret = ex.AsStatus() 27 | } 28 | quit ret 29 | } 30 | 31 | /// Add a channel. 32 | /// It must be configured in $$$Config global. 33 | ClassMethod AddChannel(pChannel As %String) As %Status 34 | { 35 | set ret = $$$OK 36 | try { 37 | $$$LOG("channel="_pChannel) 38 | 39 | do $system.Event.Create($$$ResourceName(pChannel)) 40 | set subscriber="" 41 | for { 42 | set subscriber=$order($$$Config(pChannel,"Subscriber",subscriber),1,numJobs) 43 | quit:subscriber="" 44 | 45 | $$$ThrowOnError(..AddSubscriber(pChannel, subscriber, numJobs)) 46 | } 47 | } catch ex { 48 | set ret = ex.AsStatus() 49 | } 50 | quit ret 51 | } 52 | 53 | /// Add a subscriber to a channel with a number of processes. 54 | /// It must be configured in $$$Config global. 55 | ClassMethod AddSubscriber(pChannel As %String, pSubscriber As %String, pNumJobs As %Integer) As %Status 56 | { 57 | set ret = $$$OK 58 | try { 59 | // check dead, running subscriber jobs for channel 60 | set job="", runningJobs=0, deadJobs="" 61 | for { 62 | set job=$order($$$StatusChannel(pChannel, "Job", job)) 63 | quit:job="" 64 | 65 | set process = ##class(%SYS.ProcessQuery).%OpenId(job) 66 | if process'=$$$NULLOREF { 67 | set runningJobs=runningJobs+1 68 | } else { 69 | set deadJobs=deadJobs_$listbuild(job) 70 | } 71 | } 72 | 73 | // clean dead jobs 74 | for i=1:1:$listlength(deadJobs) { 75 | $$$ThrowOnError(..CleanJob($listget(deadJobs,i))) 76 | } 77 | 78 | // create new jobs 79 | set newJobs = (pNumJobs-runningJobs) 80 | $$$LOG(pSubscriber_" config="_pNumJobs_" dead="_$ll(deadJobs)_" running="_runningJobs_" new="_newJobs) 81 | 82 | if newJobs > 0 { 83 | for i=1:1:newJobs { 84 | // start subscriber 85 | job $classmethod(pSubscriber, "Start", pChannel) 86 | 87 | // save job information 88 | set $$$StatusChannel(pChannel, "Job", $zchild)="" 89 | set $$$StatusJob($zchild, "Channel")=pChannel 90 | set $$$StatusJob($zchild, "Created")=$zdatetime($horolog,3) 91 | } 92 | } 93 | } catch ex { 94 | set ret = ex.AsStatus() 95 | } 96 | quit ret 97 | } 98 | 99 | /// Terminate all running subscriber jobs. 100 | ClassMethod TerminateAll() 101 | { 102 | set ret = $$$OK 103 | try { 104 | set job="" 105 | for { 106 | set job=$order($$$StatusJob(job)) 107 | quit:job="" 108 | $$$ThrowOnError(..Terminate(job)) 109 | } 110 | } catch ex { 111 | set ret = ex.AsStatus() 112 | } 113 | quit ret 114 | } 115 | 116 | /// Terminate a specific subscriber job. 117 | ClassMethod Terminate(pJob As %String) As %Status 118 | { 119 | set ret = $$$OK 120 | try { 121 | $$$LOG(pJob) 122 | do $system.Process.Terminate(pJob) 123 | $$$ThrowOnError(..CleanJob(pJob)) 124 | } catch ex { 125 | set ret = ex.AsStatus() 126 | } 127 | quit ret 128 | } 129 | 130 | /// Loop over $$$Queue and notify subscriber processes. 131 | /// This method should be used ONLY when system has been shut down and event queue must be reprocessed. 132 | ClassMethod RestartQueue() 133 | { 134 | set ret = $$$OK 135 | try { 136 | set channel="" 137 | for { 138 | set channel = $order($$$Queue(channel)) 139 | quit:channel="" 140 | 141 | set id = "", numMessages=0 142 | for { 143 | set id = $order($$$Queue(channel,id)) 144 | quit:id="" 145 | 146 | $$$LOG(channel_"->"_id) 147 | do $system.Event.Create($$$ResourceName(channel)) 148 | do $system.Event.Signal($$$ResourceName(channel), id) 149 | } 150 | } 151 | } catch ex { 152 | set ret = ex.AsStatus() 153 | } 154 | quit ret 155 | } 156 | 157 | /// Clean saved data for a job in Publisher-Subscriber data structures. 158 | ClassMethod CleanJob(pJob As %String) As %Status 159 | { 160 | set ret = $$$OK 161 | try { 162 | set channel = $get($$$StatusJob(pJob, "Channel")) 163 | if channel'="" { 164 | kill $$$StatusChannel(channel, "Job", pJob) 165 | } 166 | 167 | kill $$$StatusJob(pJob) 168 | } catch ex { 169 | set ret = ex.AsStatus() 170 | } 171 | quit ret 172 | } 173 | 174 | } 175 | 176 | --------------------------------------------------------------------------------