├── .gitignore ├── .project ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── lib.rs └── test.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | pubsub 4 | 5 | 6 | 7 | 8 | 9 | com.github.rustdt.ide.core.Builder 10 | 11 | 12 | 13 | 14 | 15 | com.github.rustdt.ide.core.nature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: required 3 | after_success: |- 4 | [ $TRAVIS_BRANCH = master ] && 5 | [ $TRAVIS_PULL_REQUEST = false ] && 6 | cargo doc && 7 | echo "" > target/doc/index.html && 8 | sudo pip install ghp-import && 9 | ghp-import -n target/doc && 10 | git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages 11 | env: 12 | global: 13 | secure: Mu9uI+RSo+dkwY93Uqbzr/r/Kq4PfLoWnfgLFUvgHnDrCiVyCCNxwBNiMXK4eIg9ALg2Tsa5wM1T62C5nrYws352Wml+oIXBTrfr6ec7jYV+EbdEl/KryhZk1VO6X1zeKg8HfIAKqOJ7r72RZmBUhZmrpQIlBYR9ns98WJ0hEOpIwbkiy6X3hNoKA3Uh3XhV59u9IoF5Z9lImV/Mknqf9zZsmUt/A7xXZ6ZpMD81ayJu7/S4eyQOQRSM3E11VtnHyG1bjNUXFr4VCTUSfDhmSrY39B2CnZRMH9c8WIejyk8d4jCe5mvgYBsoPw/uLXR8pZN/eROkV5V6Ilew8deAPoLgHJkziStiMLPhSurysWQ3+4haWNx6qs13NU0yrDcIEO64+m+v2IQDtxWXRwEKI4V3SO3KSYVQDM0dTwVMP6NXf5pz7QpyqMGY1b2HXUTOENGWECPn1DEkJNfzgwFhgDhBtbtUdc5Y/pC5lt0SJkCJY9mvBUjyjYiiy0dymkLTgxOxYMZ6EWTfoUDpx0bLnvbVNiJGwQzE9Yh/fMwEACsGmAS6O6qEr3NHQjv64zcSjGM5DC4jV0EevaqAtNmGVmp31JX5kShsXY87mkXVCTCmKC2pldmEWvK4VzPC4n7ZB4YEx8j8/3pqknWucyrT1j6WNb2wrkGy9UXGujxMiMk= 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "pubsub" 4 | version = "0.2.3" 5 | authors = ["Nathan Fuchs "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/fuchsnj/rust_pubsub" 9 | documentation = "http://fuchsnj.github.io/rust_pubsub" 10 | description = "Local publish / subscribe" 11 | keywords = ["pubsub", "publish", "subscribe", "local"] 12 | 13 | [dependencies] 14 | threadpool = "0.1.4" 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Nathan Fuchs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Local publish/subscribe 2 | ==== 3 | [![Build Status](https://travis-ci.org/fuchsnj/rust_pubsub.svg?branch=master)](https://travis-ci.org/fuchsnj/rust_pubsub) 4 | [![crates.io](https://img.shields.io/crates/v/pubsub.svg)](https://crates.io/crates/pubsub) 5 | [![MIT license](https://img.shields.io/crates/l/pubsub.svg)](./LICENSE) 6 | ## Documentation 7 | 8 | http://fuchsnj.github.io/rust_pubsub 9 | 10 | ## Usage 11 | 12 | Add the following to your `Cargo.toml` 13 | 14 | ```toml 15 | [dependencies] 16 | pubsub = "*" 17 | ``` 18 | 19 | and this to your crate root: 20 | 21 | ```rust 22 | extern crate pubsub; 23 | ``` 24 | 25 | ## Quick Start 26 | 27 | ```rust 28 | extern crate pubsub; 29 | use pubsub::PubSub; 30 | 31 | //create pubsub with 5 threads. These threads run notification functions. 32 | //messages are queued indefinitely until they can be run 33 | let pubsub = PubSub::new(5); 34 | 35 | //subscribe to a channel. The subscription will stay active as long 36 | //as sub1 stays in scope 37 | let sub1 = pubsub.subscribe("channel1", move |msg|{ 38 | println!("channel 1 was notified with msg: {}", msg); 39 | }); 40 | 41 | //notify all subscribers of "channel1" with message "data1" 42 | //This will always return immediately since messages are queued 43 | pubsub.notify("channel1", "data1"); 44 | 45 | //notify all subscribers of "channel1" except sub1 46 | sub1.notify_others("data3"); 47 | 48 | //If you want to start queueing messsages for a subscription, but 49 | //don't want to process them until after some initialization 50 | let sub2_activator = pubsub.lazy_subscribe("channel2"); 51 | 52 | //do some initialization here... 53 | 54 | //notifications received here are queued 55 | pubsub.notify("channel2", "data4"); 56 | 57 | //this creates a subscription and processes any messages in the backlog 58 | let sub2 = sub2_activator.activate(move |msg|{ 59 | println!("channel 2 was notified with msg: {}", msg); 60 | }); 61 | 62 | //subscriptions can be cancelled by dropping or cancelling 63 | drop(sub1); 64 | sub2.cancel(); 65 | ``` 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate threadpool; 2 | 3 | use threadpool::ThreadPool; 4 | use std::collections::HashMap; 5 | use std::sync::{Arc, Mutex}; 6 | use std::rc::Rc; 7 | use std::fmt::{Formatter, Error, Debug}; 8 | use std::collections::vec_deque::VecDeque; 9 | 10 | #[cfg(test)] 11 | mod test; 12 | 13 | pub struct Subscription{ 14 | pubsub: PubSub, 15 | channel_id: String, 16 | id: u64 17 | } 18 | 19 | impl Debug for Subscription{ 20 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error>{ 21 | fmt.write_str(&format!("Sub(channel={})", self.channel_id)) 22 | } 23 | } 24 | 25 | impl Subscription{ 26 | pub fn cancel(self){/* self is dropped */} 27 | 28 | pub fn notify_others(&self, msg: &str){ 29 | self.pubsub.notify_exception(&self.channel_id, msg, Some(self.id)); 30 | } 31 | } 32 | impl Drop for Subscription{ 33 | fn drop(&mut self){ 34 | self.pubsub.unregister(self); 35 | } 36 | } 37 | 38 | pub struct SubActivator{ 39 | sub: Subscription 40 | } 41 | impl SubActivator{ 42 | pub fn activate(self, func: F) -> Subscription 43 | where F: FnMut(String) + 'static + Send{ 44 | self.sub.pubsub.activate(&self.sub.channel_id, self.sub.id, func); 45 | self.sub 46 | } 47 | } 48 | 49 | struct SubData{ 50 | running: RAIIBool, 51 | backlog: VecDeque, 52 | func: Option>>> 53 | } 54 | 55 | struct InnerPubSub{ 56 | channels: HashMap>, 57 | //id will stay unique for hundreds of years, even at ~1 billion/sec 58 | next_id: u64, 59 | thread_pool: Rc 60 | } 61 | #[derive(Clone)] 62 | pub struct PubSub{ 63 | inner: Arc> 64 | } 65 | unsafe impl Send for PubSub{} 66 | unsafe impl Sync for PubSub{} 67 | 68 | #[derive(Clone)] 69 | struct RAIIBool{ 70 | value: Arc> 71 | } 72 | impl RAIIBool{ 73 | fn new(value: bool) -> RAIIBool{ 74 | RAIIBool{ 75 | value: Arc::new(Mutex::new(value)) 76 | } 77 | } 78 | fn set(&self, value: bool) -> bool{ 79 | let mut guard = self.value.lock().unwrap(); 80 | let old:bool = *guard; 81 | *guard = value; 82 | old 83 | } 84 | fn new_guard(&self, value: bool) -> RAIIBoolGuard{ 85 | RAIIBoolGuard::new(self.clone(), value) 86 | } 87 | } 88 | 89 | struct RAIIBoolGuard{ 90 | data: RAIIBool, 91 | value: bool 92 | } 93 | impl RAIIBoolGuard{ 94 | fn new(data: RAIIBool, value: bool) -> RAIIBoolGuard{ 95 | RAIIBoolGuard{ 96 | data: data, 97 | value: value 98 | } 99 | } 100 | fn done(self){} 101 | } 102 | impl Drop for RAIIBoolGuard{ 103 | fn drop(&mut self){ 104 | self.data.set(self.value); 105 | } 106 | } 107 | 108 | impl PubSub{ 109 | pub fn new(num_threads: usize) -> PubSub{ 110 | PubSub{ 111 | inner: Arc::new(Mutex::new(InnerPubSub{ 112 | channels: HashMap::new(), 113 | next_id: 0, 114 | thread_pool: Rc::new(ThreadPool::new(num_threads)) 115 | })) 116 | } 117 | } 118 | fn internal_subscribe(&self, channel: &str, func: Option) -> Subscription 119 | where F: FnMut(String) + 'static + Send{ 120 | let mut data = self.inner.lock().unwrap(); 121 | if !data.channels.contains_key(channel){ 122 | data.channels.insert(channel.to_string(), HashMap::new()); 123 | } 124 | let id = data.next_id; 125 | data.next_id += 1; 126 | let sub_data = SubData{ 127 | running: RAIIBool::new(false), 128 | backlog: VecDeque::new(), 129 | func: func.map(|f|Arc::new(Mutex::new(Box::new(f) as Box<_>))) 130 | }; 131 | 132 | let subscriptions = data.channels.get_mut(channel).unwrap(); 133 | subscriptions.insert(id, sub_data); 134 | Subscription{ 135 | pubsub: self.clone(), 136 | channel_id: channel.to_string(), 137 | id: id 138 | } 139 | } 140 | pub fn subscribe(&self, channel: &str, func: F) -> Subscription 141 | where F: FnMut(String) + 'static + Send{ 142 | self.internal_subscribe(channel, Some(func)) 143 | } 144 | 145 | #[allow(unused_assignments)] 146 | pub fn lazy_subscribe(&self, channel: &str) -> SubActivator{ 147 | let mut func = Some(|_|{});//used to give type info to 'func' 148 | func = None; 149 | SubActivator{ 150 | sub: self.internal_subscribe(channel, func) 151 | } 152 | } 153 | fn activate(&self, channel: &str, id: u64, func: F) 154 | where F: FnMut(String) + 'static + Send{ 155 | let mut inner = self.inner.lock().unwrap(); 156 | let pool = inner.thread_pool.clone(); 157 | let subs = inner.channels.get_mut(channel).unwrap();//channel will always exist 158 | let sub_data = subs.get_mut(&id).unwrap();//sub id will always exist 159 | sub_data.func = Some(Arc::new(Mutex::new(Box::new(func)))); 160 | self.schedule_worker(sub_data, channel, id, &pool); 161 | } 162 | pub fn num_channels(&self) -> usize{ 163 | let data = self.inner.lock().unwrap(); 164 | data.channels.len() 165 | } 166 | fn unregister(&self, sub: &Subscription){ 167 | let mut inner = self.inner.lock().unwrap(); 168 | let mut remove_channel = false; 169 | { 170 | let sub_list = inner.channels.get_mut(&sub.channel_id).unwrap(); 171 | sub_list.remove(&sub.id); 172 | if sub_list.len() == 0{ 173 | remove_channel = true; 174 | } 175 | } 176 | if remove_channel{ 177 | inner.channels.remove(&sub.channel_id); 178 | } 179 | } 180 | fn schedule_worker(&self, sub_data: &mut SubData, channel: &str, id: u64, pool: &Rc){ 181 | if !sub_data.running.set(true){//if not currently running 182 | let thread_running = sub_data.running.clone(); 183 | if let Some(func) = sub_data.func.clone(){ 184 | let pubsub = self.clone(); 185 | let channel = channel.to_string(); 186 | let id = id.clone(); 187 | pool.execute(move ||{ 188 | use std::ops::DerefMut; 189 | 190 | let finish_guard = thread_running.new_guard(false); 191 | let mut guard = func.lock().unwrap(); 192 | let mut func = guard.deref_mut(); 193 | let mut running = true; 194 | while running{ 195 | let mut notification_message = None; 196 | { 197 | let mut inner = pubsub.inner.lock().unwrap(); 198 | if let Some(subs) = inner.channels.get_mut(&channel){ 199 | if let Some(sub_data) = subs.get_mut(&id){ 200 | if let Some(msg) = sub_data.backlog.pop_front(){ 201 | notification_message = Some(msg); 202 | } 203 | } 204 | } 205 | }//unlock 'inner' 206 | if let Some(msg) = notification_message{ 207 | func(msg); 208 | }else{ 209 | running = false; 210 | } 211 | } 212 | finish_guard.done(); 213 | }); 214 | }else{ 215 | thread_running.set(false); 216 | } 217 | } 218 | } 219 | pub fn notify(&self, channel: &str, msg: &str){ 220 | self.notify_exception(channel, msg, None) 221 | } 222 | fn notify_exception(&self, channel: &str, msg: &str, exception: Option){ 223 | let mut inner = self.inner.lock().unwrap(); 224 | let pool = inner.thread_pool.clone(); 225 | if let Some(subscriptions) = inner.channels.get_mut(channel){ 226 | for (id,sub_data) in subscriptions{ 227 | if Some(*id) != exception{ 228 | sub_data.backlog.push_back(msg.to_string()); 229 | self.schedule_worker(sub_data, channel, *id, &pool); 230 | } 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use PubSub; 2 | use std::thread::sleep; 3 | use std::sync::{Arc, Mutex}; 4 | use std::time::Duration; 5 | 6 | #[test] 7 | fn basic_test() { 8 | let pubsub = PubSub::new(5); 9 | let count = Arc::new(Mutex::new(0)); 10 | { 11 | let count1 = count.clone(); 12 | let sub1 = pubsub.subscribe("channel1", move |_|{ 13 | sleep(Duration::from_millis(1000)); 14 | *count1.lock().unwrap() += 1; 15 | }); 16 | let count2 = count.clone(); 17 | let sub2 = pubsub.subscribe("channel2", move |_|{ 18 | sleep(Duration::from_millis(1000)); 19 | *count2.lock().unwrap() += 1; 20 | }); 21 | pubsub.notify("channel1", "data1"); 22 | pubsub.notify("channel1", "data2"); 23 | 24 | pubsub.notify("channel2", "data3"); 25 | pubsub.notify("channel2", "data4"); 26 | 27 | sleep(Duration::from_millis(500)); 28 | assert_eq!(*count.lock().unwrap(), 0); 29 | sub2.cancel(); 30 | sleep(Duration::from_millis(1000)); 31 | assert_eq!(*count.lock().unwrap(), 2); 32 | sleep(Duration::from_millis(1000)); 33 | assert_eq!(*count.lock().unwrap(), 3); 34 | sub1.cancel(); 35 | } 36 | assert!(pubsub.num_channels() == 0); 37 | } 38 | 39 | #[test] 40 | fn lazy_subscribe(){ 41 | let pubsub = PubSub::new(5); 42 | let count = Arc::new(Mutex::new(0)); 43 | 44 | let sub1_activator = pubsub.lazy_subscribe("channel1"); 45 | pubsub.notify("channel1", "data1"); 46 | 47 | let count1 = count.clone(); 48 | let sub1 = sub1_activator.activate(move |msg|{ 49 | assert_eq!(msg, "data1"); 50 | *count1.lock().unwrap() += 1; 51 | }); 52 | sleep(Duration::from_millis(500)); 53 | assert_eq!(*count.lock().unwrap(), 1); 54 | sub1.cancel(); 55 | } 56 | 57 | #[test] 58 | fn notify_exception() { 59 | let pubsub = PubSub::new(5); 60 | let count = Arc::new(Mutex::new(0)); 61 | 62 | let count1 = count.clone(); 63 | let sub1 = pubsub.subscribe("channel1", move |_|{ 64 | *count1.lock().unwrap() -= 1; 65 | }); 66 | let count2 = count.clone(); 67 | let sub2 = pubsub.subscribe("channel1", move |_|{ 68 | *count2.lock().unwrap() += 1; 69 | }); 70 | 71 | sub1.notify_others("data1"); 72 | 73 | sleep(Duration::from_millis(500)); 74 | assert_eq!(*count.lock().unwrap(), 1); 75 | sub1.cancel(); 76 | sub2.cancel(); 77 | } --------------------------------------------------------------------------------