├── .gitignore ├── .gitmodules ├── .gopath └── src │ └── go.polydawn.net │ └── go-sup ├── LICENSE ├── README.md ├── api.go ├── behaviors.go ├── circle.yml ├── context.go ├── docs └── dev │ └── scrabble.md ├── errors.go ├── example └── saltmines │ ├── main.go │ └── main_test.go ├── example_daemonspawner_test.go ├── example_test.go ├── goad ├── latch ├── fuse.go ├── latch.go ├── latch_bench_test.go └── testutil_test.go ├── logging.go ├── logging_test.go ├── manager.go ├── manager_internal.go ├── manager_test.go ├── phist ├── phist.go └── phist_test.go ├── sluice ├── sluice.go └── sluice_test.go └── writ.go /.gitignore: -------------------------------------------------------------------------------- 1 | ## ignore go build outputs 2 | /bin/ 3 | /.gopath/pkg/ 4 | /.gopath/tmp/ 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".gopath/src/github.com/jtolds/gls"] 2 | path = .gopath/src/github.com/jtolds/gls 3 | url = https://github.com/jtolds/gls 4 | [submodule ".gopath/src/github.com/smartystreets/goconvey"] 5 | path = .gopath/src/github.com/smartystreets/goconvey 6 | url = https://github.com/smartystreets/goconvey 7 | [submodule ".gopath/src/github.com/smartystreets/assertions"] 8 | path = .gopath/src/github.com/smartystreets/assertions 9 | url = https://github.com/smartystreets/assertions 10 | [submodule ".gopath/src/go.polydawn.net/meep"] 11 | path = .gopath/src/go.polydawn.net/meep 12 | url = https://github.com/polydawn/meep.git 13 | -------------------------------------------------------------------------------- /.gopath/src/go.polydawn.net/go-sup: -------------------------------------------------------------------------------- 1 | ../../../ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SUP 2 | === 3 | 4 | Supervisors for Go(lang). 5 | 6 | 7 | 8 | Why? 9 | ---- 10 | 11 | Collecting goroutines is important. 12 | It's simply systematically *sane*. 13 | Letting goroutines spin off into the distance with no supervision is extremely likely to represent either a resource leak, 14 | a missing error handle path, or an outright logic bug. 15 | 16 | There's been a litany [1][2] of blogs about this recently, if anyone doesn't consider this self-evident. 17 | 18 | - [1] https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop 19 | - [2] https://rakyll.org/leakingctx/ 20 | 21 | In a dream world, we might have Erlang-style supervisor trees so this can all be done with less boilerplate. 22 | It's almost even possible to do this as a library in go! 23 | 24 | And that's what go-sup is. Supervisor trees as a library. 25 | 26 | 27 | 28 | What this doesn't solve 29 | ----------------------- 30 | 31 | Quitting is still "cooperative" -- code must be well behaved, and respond to the quit in a reasonable time. 32 | 33 | In most situations, well-behaved code is not terribly complicated to write. 34 | However, blocking IO often still presents [a bit of an issue](https://news.ycombinator.com/item?id=13332185). 35 | Any supervisor of a goroutine that may be IO-blocked may itself be indefinitely stuck, and so on up-tree. 36 | Typically this can at least be salved by using timeouts to minimize the worst case for block times. 37 | 38 | go-sup will issue warnings messages (the function for this is configurable -- the default is printing to stderr) 39 | for tasks that do not return within a reasonable time (2 seconds). 40 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | /* 2 | The go-sup package provides a "supervisor" system for easily and safely 3 | launching concurrent tasks, while also reliably handling errors, and 4 | simplifying concurrent shutdowns. 5 | 6 | To make use of go-sup, your application functions should implement 7 | the `Agent` interface: this accepts a `Supervisor`. 8 | 9 | `Supervisor` provides a name to your task (for logging), and channels 10 | which your application can check to see if this service should quit. 11 | (This is necessary for orderly shutdown in complex applications.) 12 | 13 | Start your application with `sup.NewTask(yourAgentFn)`. 14 | 15 | Whenever your application needs to split off more worker goroutines, 16 | create a `Manager`. Use the `Manager.NewTask` function to set up 17 | supervision for those tasks, then kick them off in parallel with `go`. 18 | You can wait for all the tasks, and handle their errors, using the 19 | manager. 20 | 21 | Build the rest of your application flow with channels as normal. 22 | go-sup stays out of the way, and provides control channels you can 23 | use in your application selects if you need to... 24 | or you can simply ignore them, if you don't need fancy handling. 25 | 26 | That's it. Error handling made easy; quit signals built in throughout. 27 | */ 28 | package sup 29 | 30 | import ( 31 | "go.polydawn.net/go-sup/sluice" 32 | ) 33 | 34 | /* 35 | Your functions! 36 | */ 37 | type Agent func(Supervisor) 38 | 39 | /* 40 | A Writ is the authority and the setup for a supervisor -- create one, 41 | then use it to run your `Agent` function. 42 | 43 | Use `NewTask` to get started at the top of your program. 44 | After that, any of your agent functions that wants to delegate work 45 | should create a `Manager`, then launch tasks via `Manager.NewTask`. 46 | */ 47 | type Writ interface { 48 | /* 49 | There's a bigger reason for the `Writ` type than vanity naming: 50 | The main reason so that we can capture the intention to run a 51 | function immediately, even if the `Run` itself shall be kicked to 52 | the other side of a new goroutine. 53 | 54 | When using managers, this is necessary in order to make sure we start 55 | every task we intended to! 56 | 57 | The `Agent` describing the real work to do is given as a parameter to 58 | `Writ.Run` instead of taken as a parameter at creation time 59 | to make sure you don't accidentally pass in the agent 60 | function and then forget to call the real 'go-do-it' method afterwards. 61 | We need two methods because in the statement `go mgr.NewTask(taskfn)`, 62 | the `NewTask` call is not evaluated until arbitrarily later, yet 63 | for system-wide sanity and graceful termination, we need to be able 64 | to declare "no new tasks accepted" at a manager... and then return, 65 | but of course only after all already-started tasks are done. 66 | If task start may arbitrarily delayed, you can see the race: this is 67 | why we need a register step and a run step in separate methods. 68 | */ 69 | 70 | /* 71 | Returns the name assigned to this writ when it was created. 72 | Names are a list of strings, typically indicating the hierarchy of 73 | managers the writ was created under. 74 | */ 75 | Name() WritName 76 | 77 | /* 78 | Do the duty: run the given function using the current goroutine. 79 | Errors will be captured, etc; you're free (advised, even) to 80 | run this in a new goroutine e.g. `go thewrit.Run(anAgentFunc)` 81 | 82 | Returns self, to enable chaining if desired. 83 | */ 84 | Run(Agent) Writ 85 | 86 | /* 87 | Cancel the writ. This will cause supervisor handed to a `Run` agent 88 | to move to its quitting state. 89 | 90 | Returns self, to enable chaining if desired. 91 | */ 92 | Cancel() Writ 93 | 94 | /* 95 | Return the error that was panicked from the running agent, or, wait 96 | until the agent has returned without error (in which case the result 97 | is nil). 98 | */ 99 | Err() error 100 | 101 | /* 102 | Return a channel which will be closed when the writ becomes done. 103 | */ 104 | DoneCh() <-chan struct{} 105 | } 106 | 107 | /* 108 | Prepare a new task -- it will answer to no one 109 | and will only be cancelled by your hand. 110 | 111 | Use this to manually supervise over a single Agent. 112 | If launching multiple Agents, create a `Manager` and use `Manager.NewTask()`. 113 | A Manager can help monitor, control, and cancel many tasks at once. 114 | 115 | Typically, `sup.NewTask()` is used only once -- at the start of your program. 116 | */ 117 | func NewTask(name ...string) Writ { 118 | writName := WritName{} 119 | for _, n := range name { 120 | writName = writName.New(n) 121 | } 122 | return newWrit(writName) 123 | } 124 | 125 | /* 126 | The interface workers look up to in order to determine when they can retire. 127 | */ 128 | type Supervisor interface { 129 | Name() WritName 130 | Quit() bool 131 | QuitCh() <-chan struct{} 132 | } 133 | 134 | type Manager interface { 135 | /* 136 | Tell the manager there's new stuff to do; get a writ you can use to run 137 | your new stuff under the manager's supervision. 138 | 139 | The writ may actually be a no-op'er, if the manager is no longer 140 | accepting new wards. 141 | */ 142 | NewTask(name string) Writ 143 | 144 | /* 145 | Halt accepting new work, and service all existing children. 146 | Errors raised by any children will cause the manager to cancel all 147 | other children and raise that first error -- returning (or panicking) 148 | only after all children have returned. 149 | */ 150 | Work() 151 | 152 | /* 153 | Return a channel that will eventually return a `Writ` of a completed 154 | child function. 155 | 156 | This is a `go-sup/sluice` channel -- be careful about discarding it; 157 | the chan will *eventually* soak a value, whether you receive it or not. 158 | 159 | The returned value will be a `Writ` -- the `sluice.T` type is an 160 | artifact of generics not being a thing. 161 | 162 | You don't need to call this if you find the error handling of `Work` 163 | to be acceptable. 164 | */ 165 | GatherChild() <-chan sluice.T 166 | 167 | // TODO i do believe you who initialized this thing ought to be able to cancel it as well. 168 | // at the same time, no you can't cancel individual supervisors its spawned for agents you've delegated, because wtf is that mate. 169 | } 170 | 171 | func NewManager(reportingTo Supervisor) Manager { 172 | return newManager(reportingTo) 173 | } 174 | 175 | /* 176 | Sets the log function used for internal debug messages. 177 | 178 | Don't call this in libraries. 179 | If you do call it in a program, do so as early as possible; 180 | if you fail to set this before starting any tasks or supervisors 181 | (or spinning off any goroutines that will do so), then your logs may be 182 | of mixed format, and if you have the race detector enabled you will most 183 | certainly find one. 184 | */ 185 | func SetLogFunction(fn LogFn) { 186 | log = fn 187 | } 188 | -------------------------------------------------------------------------------- /behaviors.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | /* 4 | Gathering type to hang methods off of. 5 | 6 | Typical useage is via 7 | 8 | sup.Behaviors.Looper([...]) 9 | */ 10 | type Behavior struct{} 11 | 12 | var Behaviors Behavior 13 | 14 | //// Looper 15 | 16 | /* 17 | Decorates an agent to be invoked in a loop, so long as 18 | the supervisor hasn't signalled it's time to quit. 19 | */ 20 | func (Behavior) Looper(agent Agent) Agent { 21 | return looper{agent}.Work 22 | } 23 | 24 | type looper struct{ Agent } 25 | 26 | func (x looper) Work(super Supervisor) { 27 | for !super.Quit() { 28 | x.Agent(super) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - ./goad init 4 | test: 5 | override: 6 | - ./goad install 7 | - ./goad test 8 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | /* 8 | Copy of the stdlib (or `x/net`, in older go versions) `Context` interface. 9 | 10 | Cloned here simply so we can assert that we implement it (without requiring 11 | either that package import or go1.7 for build) -- and not exported because 12 | if you care about this, use those packages to refer to it. 13 | 14 | Upstream: https://tip.golang.org/src/context/context.go 15 | */ 16 | type context interface { 17 | // Returns a deadline, if there is one. (go-sup doesn't use these.) 18 | Deadline() (deadline time.Time, ok bool) 19 | 20 | // Select on this to know when you should return. 21 | Done() <-chan struct{} 22 | 23 | // Name and type somewhat confusing: this is a nilipotent checker for if `Done` already happened 24 | Err() error 25 | 26 | // Bag for values. (go-sup doesn't use these.) https://blog.golang.org/context has suggested usage. 27 | Value(key interface{}) interface{} 28 | } 29 | -------------------------------------------------------------------------------- /docs/dev/scrabble.md: -------------------------------------------------------------------------------- 1 | 2 | ### Prior excellent discussions of channels and how to think about their semantics: 3 | 4 | - https://inconshreveable.com/07-08-2014/principles-of-designing-go-apis-with-channels/ 5 | - In particular, that it's often most correct for functions to *accept* a writable channel as a parameter instead of returning one, 6 | because this kind of gather pattern means selecting over bulks of these events works fine. 7 | - http://blog.golang.org/pipelines 8 | - Discussions of patterns for 'Done' channels and cancellation 9 | 10 | ### various channel use strategies 11 | 12 | - Closing a chan is the only way to send an event to $n receivers at once 13 | - It's also incapable of blocking the closer, which is hugely useful for systemic reliability. 14 | - but also means you must *return* a chan, which is incompatible with selecting :( 15 | - maybe a pattern of supervisors (or lighter fold-in'ers) that gathers unblockable close ops into a report stream is useful. "wastes" a goroutine, but.. reliable. 16 | - ... it might be useful to combine this with latchery: unblockable close ops where they matter; one (standard) fan out worker that can warn on slow recvrs 17 | - Accepting a chan for gathering is great 18 | - ... but now if you care about anything but the sheer number of events, the chan's message now needs to contain meaning (as opposed to a close-only signalling chan, where uniqueness yada yada) 19 | - hello there generics problem 20 | - you also must keep a list of each chan you've accepted, now, and sync around that (it's not bounded). 21 | - (repeatr already did this in the knowledge-base system in several places, it appears to be a common pattern) 22 | 23 | ### compare: x/net/context 24 | 25 | - By and large: this is the right direction, absolutely. But it's not everything. 26 | - Bugs me: The factory methods are kind of helterskelter, and I shouldn't have to implement those for new context types. 27 | - This is perhaps nitpicky, but I'd really prefer the API strongly guides you towards deadlines (a relative timeout function should be *more* typing, because it's almost always less correct). 28 | - Wouldn't it make more sense if you constructed new ones and handed them a ref uptree, since that's what *all the rest* of your regular funcs act like. 29 | - The "Value" bag thing is something I'd really rather remained a separate concept. 30 | - Overall, this is a system for doing cancellations, and that's fine, but it doesn't have much to say about collecting errors on the way up again, and that is a thing I'm interested in regularizing. 31 | - There's sense to this: x/net/context is a parameter you pass down, and that's it, and you still use control flow for returns... and by doing so, you never get stuck in the generics desire. 32 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | import ( 4 | "go.polydawn.net/meep" 5 | ) 6 | 7 | type ErrTaskPanic struct { 8 | meep.TraitAutodescribing 9 | meep.TraitCausable 10 | 11 | // The first ErrTaskPanic keeps traces, but if a series of them are 12 | // raised (as the manager hierarchy frequently does), the latter errors 13 | // will skip collecting their stacks, leaving this trait blank. 14 | meep.TraitTraceable 15 | 16 | // The name of the task that panicked. 17 | // (If this task had a manager, its name is one level up from this one.) 18 | Task WritName 19 | } 20 | -------------------------------------------------------------------------------- /example/saltmines/main.go: -------------------------------------------------------------------------------- 1 | package saltmines 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | 8 | "go.polydawn.net/go-sup" 9 | ) 10 | 11 | func Main(stdin io.Reader, stderr io.Writer) { 12 | // I'm a very lazy controller, and I mostly delegate work to others. 13 | // I don't actually even wake up if something goes wrong! 14 | // I assume things are going according to plan unless something 15 | // really terrible gets so far that it sets my office on fire around me. 16 | // Even then: honestly, I'm already in the Cayman Islands. There's 17 | // nobody in the head office anymore. Really, all the other workers 18 | // need is the idea that there's someone they *could* complain to. 19 | // If somebody actually *does* file a report, the mail will make 20 | // it to the corporate franchise office, somehow. (Maybe the dutiful 21 | // secretary I left behind will actually do the maint work for me, 22 | // even though I've nipped off.) 23 | fmt.Fprintf(stderr, "Owner: hello\n") 24 | 25 | // There are four major operations going on under my domain: 26 | // - The mining pits -- these produce a steady stream of "slag" 27 | // - The ore washing plants -- these do some basic processing, and route out several different kinds of "ore" 28 | // - The foundries -- there's several different kinds of these, they take only specific kinds of "ore" 29 | // - The shipping wharf -- this station packages up all the ingots into crates for sale 30 | // Keep an eye on the ore washing plants. Sometimes they get jammed, 31 | // and we have to take one out of service, scrap it for parts, and 32 | // just install a whole new one without any of the wear-n-tear. 33 | // There's also a fivth operation: the oversight office. 34 | // The oversight office can sometimes get letters from other parts 35 | // of Her Majesty's Goverment, to which the office is required to 36 | // respond in a timely fashion. Sometimes this requires the 37 | // oversight office to commission a team to gather a report. Such 38 | // teams tend to be short-lived, but they may ask questions about 39 | // (or sometimes give odd orders to) the other three major operational 40 | // centers of our production pipeline. 41 | rootWrit := sup.NewTask() 42 | rootWrit.Run(func(super sup.Supervisor) { 43 | mgr := sup.NewManager(super) 44 | 45 | slagPipe := make(chan Slag) 46 | minePit := &MinePits{ 47 | thePit: stdin, 48 | slagPipe: slagPipe, 49 | } 50 | go mgr.NewTask("minePit").Run(minePit.Run) 51 | //minePitWitness.Cancel() 52 | 53 | oreWasher := &OreWashingFacility{ 54 | slagPipe: slagPipe, 55 | copperHopper: make(chan OreCopper), 56 | tinHopper: make(chan OreTin), 57 | zincHopper: make(chan OreZinc), 58 | } 59 | go mgr.NewTask("oreWasher").Run(oreWasher.Run) 60 | //oreWasherWitness.Cancel() 61 | 62 | rootWrit.Cancel() 63 | mgr.Work() 64 | }) 65 | } 66 | 67 | type ( 68 | Slag string 69 | 70 | OreCopper string 71 | OreTin string 72 | OreZinc string 73 | 74 | IngotCopper string 75 | IngotTin string 76 | IngotZinc string 77 | 78 | Crate struct{ ingots []string } 79 | ) 80 | 81 | type MinePits struct { 82 | thePit io.Reader 83 | slagPipe chan<- Slag 84 | } 85 | 86 | func (mp *MinePits) Run(svr sup.Supervisor) { 87 | scanner := bufio.NewScanner(mp.thePit) 88 | scanner.Split(bufio.ScanWords) 89 | for { 90 | // intentionally evil example. we need interruptable readers to 91 | // be able to shut down truly gracefully. 92 | select { 93 | default: 94 | scanner.Scan() 95 | // careful. you have to put nb/cancellable selects for each send, too. 96 | select { 97 | case mp.slagPipe <- Slag(scanner.Text()): 98 | case <-svr.QuitCh(): 99 | } 100 | case <-svr.QuitCh(): 101 | return 102 | } 103 | } 104 | } 105 | 106 | type OreWashingFacility struct { 107 | slagPipe <-chan Slag 108 | copperHopper chan<- OreCopper 109 | tinHopper chan<- OreTin 110 | zincHopper chan<- OreZinc 111 | } 112 | 113 | func (owf *OreWashingFacility) Run(svr sup.Supervisor) { 114 | // Ore washing is a slow process, and sometimes a batch takes quite 115 | // some time; this can strike fairly randomly, so we run a bunch 116 | // of processing separately to even things out. 117 | // That means *we're* a supervisor for all those parallel processors. 118 | mgr := sup.NewManager(svr) 119 | for n := 0; n < 4; n++ { 120 | go mgr.NewTask(fmt.Sprintf("wshr-%02d", n)).Run(owf.runSingleStation) 121 | } 122 | mgr.Work() 123 | } 124 | 125 | func (owf *OreWashingFacility) runSingleStation(svr sup.Supervisor) { 126 | for { 127 | select { 128 | case slag := <-owf.slagPipe: 129 | // this looks a little squishy, but keep in mind 130 | // the level of contrivance here. it's quite unlikely 131 | // that one would ever write a real typed fanout so trivial as this. 132 | switch slag { 133 | case "copper": 134 | select { 135 | case owf.copperHopper <- OreCopper(slag): 136 | case <-svr.QuitCh(): 137 | } 138 | case "tin": 139 | select { 140 | case owf.tinHopper <- OreTin(slag): 141 | case <-svr.QuitCh(): 142 | } 143 | case "zinc": 144 | select { 145 | case owf.zincHopper <- OreZinc(slag): 146 | case <-svr.QuitCh(): 147 | } 148 | default: 149 | panic(fmt.Sprintf("unknown ore type %q, cannot sort", slag)) 150 | } 151 | case <-svr.QuitCh(): 152 | return 153 | } 154 | } 155 | } 156 | 157 | type FoundryCoordinator struct { 158 | } 159 | 160 | type OversightOffice struct { 161 | } 162 | -------------------------------------------------------------------------------- /example/saltmines/main_test.go: -------------------------------------------------------------------------------- 1 | package saltmines 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | /* 11 | Some drills you could imagine running on our little manufactory: 12 | 13 | - normal operation, until the mines run out and all work is done 14 | - normal operation, terminated by quit signals at the top 15 | - a fire in the foundry control office requires us to shut down all foundries 16 | - an ore washing plant jams and must be replaced 17 | - the oversight office gets some requests 18 | - the oversight office is in the process of gathering production reports, 19 | when there's an accident in the mines; the report must still go out 20 | - a fuse blows in the ore washing station and *all* those plants stop for 21 | a while. The mines are backed up, and the foundries waiting. 22 | If there's a fire in the foundries at the same time, and we should 23 | surely still be able to evacuate everyone in a timely fashion 24 | */ 25 | 26 | func ExampleSaltmines() { 27 | defer fmt.Printf("Example: complete!") 28 | input := bytes.NewBufferString("copper copper") 29 | Main(input, io.MultiWriter(os.Stdout, os.Stderr)) 30 | 31 | // Output: 32 | // Owner: hello 33 | // Example: complete! 34 | } 35 | -------------------------------------------------------------------------------- /example_daemonspawner_test.go: -------------------------------------------------------------------------------- 1 | package sup_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "." 8 | ) 9 | 10 | func ExampleDaemonSpawner() { 11 | type bigTask string 12 | 13 | var workerFactory = func(task bigTask) sup.Agent { 14 | return func(super sup.Supervisor) { 15 | // has lots to do, i'm sure 16 | time.Sleep(time.Millisecond * 100) 17 | } 18 | } 19 | 20 | firehose := make(chan bigTask) 21 | cntWorkFound := 0 22 | var workFinder sup.Agent = sup.Behaviors.Looper(func(super sup.Supervisor) { 23 | select { 24 | case firehose <- bigTask(fmt.Sprintf("work-%02d", cntWorkFound)): 25 | cntWorkFound++ 26 | if cntWorkFound >= 12 { 27 | close(firehose) 28 | panic(nil) 29 | } 30 | case <-super.QuitCh(): 31 | return 32 | } 33 | }) 34 | 35 | var daemonMaster sup.Agent = func(super sup.Supervisor) { 36 | mgr := sup.NewManager(super) 37 | for { 38 | select { 39 | case <-super.QuitCh(): 40 | goto procede 41 | case todo, ok := <-firehose: 42 | if !ok { 43 | goto procede 44 | } 45 | taskName := string(todo) 46 | go mgr.NewTask(taskName).Run(workerFactory(todo)) 47 | } 48 | // do i need to 'step' periodically here? how important is manager state maint? 49 | // or check if it has any errors to raise, even if we're not done accepting? yes that's important. 50 | } 51 | procede: 52 | fmt.Printf("daemonMaster wrapping up\n") 53 | defer fmt.Printf("daemonMaster returned\n") 54 | mgr.Work() 55 | } 56 | 57 | rootWrit := sup.NewTask() 58 | rootWrit.Run(func(super sup.Supervisor) { 59 | mgr := sup.NewManager(super) 60 | go mgr.NewTask("workFinder").Run(workFinder) 61 | go mgr.NewTask("daemonMaster").Run(daemonMaster) 62 | mgr.Work() 63 | }) 64 | 65 | // Output: 66 | // daemonMaster wrapping up 67 | // daemonMaster returned 68 | } 69 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sup_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "." 8 | ) 9 | 10 | func ExampleWow() { 11 | type bid string 12 | salesFunnel := make(chan bid) 13 | var salesMinion sup.Agent = sup.Behaviors.Looper(func(super sup.Supervisor) { 14 | select { 15 | case salesFunnel <- "sale": 16 | case <-super.QuitCh(): 17 | return 18 | } 19 | }) 20 | var salesDirector sup.Agent = func(super sup.Supervisor) { 21 | mgr := sup.NewManager(super) 22 | go mgr.NewTask("sales1").Run(salesMinion) 23 | go mgr.NewTask("sales2").Run(salesMinion) 24 | go mgr.NewTask("sales3").Run(salesMinion) 25 | mgr.Work() 26 | //sup.Funnel().Gather(mgr.DoneCh()).Await() 27 | } 28 | 29 | rootWrit := sup.NewTask() 30 | rootWrit.Run(func(super sup.Supervisor) { 31 | mgr := sup.NewManager(super) 32 | go mgr.NewTask("region-a").Run(salesDirector) 33 | go mgr.NewTask("region-b").Run(salesDirector) 34 | go mgr.NewTask("region-c").Run(salesDirector) 35 | salesCnt := 0 36 | go mgr.NewTask("planner").Run(func(super sup.Supervisor) { 37 | for { 38 | select { 39 | case sale := <-salesFunnel: 40 | fmt.Fprintf(os.Stdout, "%s %d!\n", sale, salesCnt) 41 | salesCnt++ 42 | if salesCnt >= 10 { 43 | fmt.Fprintf(os.Stderr, "trying to wrap after %s!\n", sale) 44 | rootWrit.Cancel() 45 | return 46 | } 47 | case <-super.QuitCh(): 48 | return 49 | } 50 | } 51 | }) 52 | mgr.Work() 53 | }) 54 | go func() { salesFunnel <- "last" }() 55 | fmt.Printf("%s!\n", <-salesFunnel) 56 | 57 | // Output: 58 | // sale 0! 59 | // sale 1! 60 | // sale 2! 61 | // sale 3! 62 | // sale 4! 63 | // sale 5! 64 | // sale 6! 65 | // sale 7! 66 | // sale 8! 67 | // sale 9! 68 | // last! 69 | } 70 | -------------------------------------------------------------------------------- /goad: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | ### Project details 5 | name="go-sup" 6 | pkg="go.polydawn.net/$name" 7 | cmd="$pkg" 8 | 9 | 10 | 11 | ### Normalize path -- all work should be relative to this script's location. 12 | ## Set up gopath -- also relative to this dir, so we work in isolation. 13 | cd "$( dirname "${BASH_SOURCE[0]}" )" 14 | export GOPATH="$PWD/.gopath/" 15 | export GOBIN="$PWD/bin/" 16 | 17 | 18 | 19 | ### other config scripts? invoke here. 20 | ## pass pointer to project root dir down, for tests (tests run in varying CWDs, so need this hint) 21 | export PROJ="$PWD" 22 | ## use LDFLAGS to inject vars at compile time. 23 | LDFLAGS="" 24 | 25 | 26 | 27 | ### Last bits of our flag parsery. 28 | # subcommand arg? 29 | SUBCOMMAND=${1:-} 30 | # subsection arg? 31 | SUBSECTION=${2:-"..."} 32 | SUBSECTION="./$SUBSECTION" 33 | # default test timeouts are far too high. override this if you like. 34 | TEST_TIMEOUT="${TEST_TIMEOUT:-"35s"}" 35 | 36 | ### action begins! 37 | if [ -z "$SUBCOMMAND" ] ; then 38 | ( 39 | go fmt "$SUBSECTION" 40 | go install -race -ldflags "$LDFLAGS" "$cmd" && { 41 | echo -e "\E[1;32minstall successful.\E[0;m\n" 42 | } || { 43 | echo -e "\E[1;41minstall failed!\E[0;m" 44 | exit 8 45 | } 46 | go test "$SUBSECTION" -race -timeout="$TEST_TIMEOUT" && { 47 | echo -e "\n\E[1;32mall tests green.\E[0;m" 48 | } || { 49 | echo -e "\n\E[1;41msome tests failed!\E[0;m" 50 | exit 4 51 | } 52 | ) 53 | else 54 | shift # munch $subcommand from passing on in "$@" 55 | case "$SUBCOMMAND" in 56 | -) 57 | # passthrough for other commands 58 | go "$@" 59 | ;; 60 | env) 61 | echo "GOROOT=`go env GOROOT`" 62 | echo "GOPATH=`go env GOPATH`" 63 | ;; 64 | path) 65 | echo "$GOPATH" 66 | ;; 67 | init) 68 | # it's your responsibility to do this the first time 69 | # (we don't do it at the front of every build because it will move submodules if you already have them, and that might not be what you want as you're plowing along) 70 | git submodule update --init 71 | # also make sure the self-symlink exists. should be committed anyway (but then, this is also useful for project-first-steps.) 72 | mkdir -p "$(dirname ".gopath/src/$pkg")" 73 | ln -snf "$(echo "${pkg//[^\/]}/" | sed s#/#../#g)"../ ".gopath/src/$pkg" 74 | ;; 75 | test) 76 | set +e ; shift ; set -e # munch $subsection from passing on in "$@" 77 | go test -i "$SUBSECTION" -race "$@" && 78 | go test -v "$SUBSECTION" -race -timeout="$TEST_TIMEOUT" "$@" && { 79 | echo -e "\n\E[1;32mall tests green.\E[0;m" 80 | } || { 81 | echo -e "\n\E[1;41msome tests failed!\E[0;m" 82 | exit 4 83 | } 84 | ;; 85 | install) 86 | go install -race -ldflags "$LDFLAGS" "$cmd" 87 | ;; 88 | final) 89 | go install -ldflags "$LDFLAGS" "$cmd" 90 | ;; 91 | bench) 92 | profPath="$GOPATH/tmp/prof/" 93 | mkdir -p "$profPath" 94 | set +e ; shift ; set -e # munch $subsection from passing on in "$@" 95 | go test -i "$SUBSECTION" "$@" && 96 | GOCONVEY_REPORTER=silent \ 97 | go test \ 98 | -run=XXX -bench=. \ 99 | -o "$profPath/bench.bin" \ 100 | -cpuprofile="$profPath/cpu.pprof" \ 101 | "$SUBSECTION" "$@" || { 102 | echo -e "\E[1;41msome benchmarks failed!\E[0;m" 103 | exit 4 104 | } 105 | # use e.g.: go tool pprof --text .gopath/tmp/prof/bench.bin .gopath/tmp/prof/cpu.pprof 106 | ;; 107 | fmt) 108 | go fmt "$SUBSECTION" 109 | ;; 110 | doc) 111 | set +e ; shift ; set -e # munch $subsection from passing on in "$@" 112 | for package in $(go list "$SUBSECTION" | sed "s#^_${PWD}#${pkg}#"); do 113 | echo -e "==== $package ====\n" 114 | godoc "$@" "$package" 115 | echo -e "\n\n\n" 116 | done 117 | ;; 118 | cover) 119 | coverFile="$GOPATH/tmp/cover/cover.out" 120 | mkdir -p "$(dirname "$coverFile")" 121 | for package in $(go list "$SUBSECTION" | sed "s#^_${PWD}#${pkg}#"); do 122 | rm -f "$coverFile" 123 | echo "==== $package ====" 124 | go test -coverprofile="$coverFile" "$package" && \ 125 | [ -f "$coverFile" ] && \ 126 | echo ---- && \ 127 | go tool cover -func="$coverFile" && \ 128 | echo ---- && \ 129 | go tool cover -html="$coverFile" 130 | echo ==== 131 | echo 132 | done 133 | rm -f "$coverFile" 134 | ;; 135 | clean) 136 | rm -rf "$GOPATH/bin" "$GOPATH/pkg" "$GOPATH/tmp" 137 | ;; 138 | *) 139 | echo "Usage: $0 {init|test|install|bench|fmt|doc|cover|clean}" 1>&2; 140 | exit 1 141 | ;; 142 | esac 143 | fi 144 | -------------------------------------------------------------------------------- /latch/fuse.go: -------------------------------------------------------------------------------- 1 | package latch 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | type Fuse interface { 8 | Fire() 9 | IsBlown() bool 10 | Selectable() <-chan struct{} 11 | } 12 | 13 | type fuse struct { 14 | ch chan struct{} 15 | // single CAS field instead of sync.Once or even sync.Mutex, because 16 | // we have a very simple application and need precisely nothing more. 17 | // a defer tacks on about 120ns on a scale where our entire purpose 18 | // takes about 65ns. just ain't gilding we need for an unfailable op. 19 | done int32 20 | } 21 | 22 | func NewFuse() *fuse { 23 | return &fuse{ch: make(chan struct{})} 24 | } 25 | 26 | func (f *fuse) Fire() { 27 | if !atomic.CompareAndSwapInt32(&f.done, 0, 1) { 28 | return 29 | } 30 | close(f.ch) 31 | return 32 | } 33 | 34 | func (f *fuse) IsBlown() bool { 35 | return atomic.LoadInt32(&f.done) == 1 36 | } 37 | 38 | func (f *fuse) Selectable() <-chan struct{} { 39 | return f.ch 40 | } 41 | -------------------------------------------------------------------------------- /latch/latch.go: -------------------------------------------------------------------------------- 1 | package latch 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | /* 8 | Sign up for a signal, which can be flipped exactly once. 9 | 10 | Much like a `sync.WaitGroup`, except it uses channels and is thus 11 | selectable; 12 | much like a `sync.Condition`, except if someone asks to wait after 13 | the condition has been fired, they will also immediately act as signalled. 14 | 15 | This is often useful for coordinating things that have "happens-after" 16 | requirements -- for example modelling state machine transitions 17 | where we want to act only when "reached ready state (and maybe further)". 18 | 19 | A `Latch` is distinct from a `Fuse` because 20 | `Fuse.Fire` can always complete immediately and never blocks; 21 | `Latch.Trigger` sends messages to a channel and thus may block. 22 | If you can use a `Fuse`, prefer to; 23 | a `Latch` is necessary if you want to fan-in events to a gathering channel. 24 | In other words, you can't easily select on $n `Fuse`s because you have $n channels; 25 | whereas with a `Latch` you can tell $n `Latch`es to trigger just 1 channel and 26 | selecting for any of those latches to close is easy. 27 | */ 28 | type Latch interface { 29 | // Block until the latch is flipped. 30 | Wait() 31 | 32 | // Submit a channel to be signalled as soon as the latch is flipped. 33 | WaitSelectably(bellcord chan<- interface{}) 34 | 35 | // Fire the signal. If this is called more than once, it will panic (much like closing a closed channel). 36 | Trigger() 37 | 38 | // Like `Trigger`, but simply no-ops if triggering has already happened. Use sparingly. 39 | MaybeTrigger() 40 | } 41 | 42 | func New() Latch { 43 | return &latch{ 44 | bellcords: make([]chan<- interface{}, 0, 1), 45 | } 46 | } 47 | 48 | func NewWithMessage(msg interface{}) Latch { 49 | return &latch{ 50 | msg: msg, 51 | bellcords: make([]chan<- interface{}, 0, 1), 52 | } 53 | } 54 | 55 | type latch struct { 56 | mu sync.Mutex 57 | msg interface{} 58 | bellcords []chan<- interface{} 59 | } 60 | 61 | func (l *latch) Trigger() { 62 | l.mu.Lock() 63 | defer l.mu.Unlock() 64 | if l.bellcords == nil { 65 | panic("cannot repeatedly trigger latch") 66 | } 67 | l.trigger() 68 | } 69 | 70 | func (l *latch) MaybeTrigger() { 71 | l.mu.Lock() 72 | l.trigger() 73 | l.mu.Unlock() 74 | } 75 | 76 | func (l *latch) trigger() { 77 | for _, bellcord := range l.bellcords { 78 | bellcord <- l.msg 79 | } 80 | l.bellcords = nil 81 | } 82 | 83 | func (l *latch) WaitSelectably(bellcord chan<- interface{}) { 84 | l.mu.Lock() 85 | defer l.mu.Unlock() 86 | if l.bellcords == nil { 87 | // even if we're doing immediate ack, we must do so in a goroutine 88 | // in case the caller handed us an unbuffered channel which they expect 89 | // to wait on moments after this submission. Triggers don't come from 90 | // the listener-submitting routine; neither should after-trigger acks. 91 | go func() { bellcord <- l.msg }() 92 | return 93 | } 94 | l.bellcords = append(l.bellcords, bellcord) 95 | } 96 | 97 | func (l *latch) Wait() { 98 | bellcord := make(chan interface{}) 99 | l.WaitSelectably(bellcord) 100 | <-bellcord 101 | } 102 | -------------------------------------------------------------------------------- /latch/latch_bench_test.go: -------------------------------------------------------------------------------- 1 | package latch 2 | 3 | import ( 4 | "encoding/json" 5 | "runtime" 6 | "testing" 7 | ) 8 | 9 | func init() { 10 | runtime.GOMAXPROCS(4) 11 | } 12 | 13 | /* 14 | Note: running with `-benchtime 200ms` (or even 100) may be a perfectly 15 | valid time-saving choice; more does not appear to significantly 16 | improve the consistency of results. 17 | 18 | RESULTS 19 | 20 | BenchmarkLatchAllocation-4 1000000 246 ns/op 56 B/op 2 allocs/op 21 | BenchmarkBaseline_JsonUnmarshalling-4 100000 3106 ns/op 312 B/op 5 allocs/op 22 | BenchmarkLatchTriggerOnly_0BufDeadend-4 2000000 188 ns/op 0 B/op 0 allocs/op 23 | BenchmarkLatchTriggerOnly_1BufDeadend-4 1000000 254 ns/op 0 B/op 0 allocs/op 24 | BenchmarkLatchTriggerOnly_2BufDeadend-4 1000000 315 ns/op 0 B/op 0 allocs/op 25 | BenchmarkLatchTriggerOnly_4BufDeadend-4 1000000 464 ns/op 0 B/op 0 allocs/op 26 | BenchmarkLatchTriggerOnly_8BufDeadend-4 500000 735 ns/op 0 B/op 0 allocs/op 27 | BenchmarkLatchTriggerOnly_0UnbufGather-4 2000000 192 ns/op 0 B/op 0 allocs/op 28 | BenchmarkLatchTriggerOnly_1UnbufGather-4 500000 611 ns/op 0 B/op 0 allocs/op 29 | BenchmarkLatchTriggerOnly_2UnbufGather-4 300000 903 ns/op 0 B/op 0 allocs/op 30 | BenchmarkLatchTriggerOnly_4UnbufGather-4 200000 1647 ns/op 0 B/op 0 allocs/op 31 | BenchmarkLatchTriggerOnly_8UnbufGather-4 100000 2896 ns/op 0 B/op 0 allocs/op 32 | BenchmarkLatchSubscribe_1BufDeadend-4 1000000 452 ns/op 128 B/op 4 allocs/op 33 | BenchmarkLatchSubscribe_2BufDeadend-4 300000 1021 ns/op 272 B/op 9 allocs/op 34 | BenchmarkLatchSubscribe_4BufDeadend-4 200000 2095 ns/op 560 B/op 18 allocs/op 35 | BenchmarkLatchSubscribe_8BufDeadend-4 100000 4005 ns/op 1136 B/op 35 allocs/op 36 | BenchmarkLatchFullCycle_0BufDeadend-4 2000000 189 ns/op 0 B/op 0 allocs/op 37 | BenchmarkLatchFullCycle_1BufDeadend-4 500000 693 ns/op 128 B/op 4 allocs/op 38 | BenchmarkLatchFullCycle_2BufDeadend-4 200000 1321 ns/op 272 B/op 9 allocs/op 39 | BenchmarkLatchFullCycle_4BufDeadend-4 100000 2506 ns/op 560 B/op 18 allocs/op 40 | BenchmarkLatchFullCycle_8BufDeadend-4 100000 4712 ns/op 1136 B/op 35 allocs/op 41 | BenchmarkFuseTriggerOnly_0Waiters-4 5000000 62.4 ns/op 0 B/op 0 allocs/op 42 | BenchmarkFuseTriggerOnly_1Waiters-4 1000000 352 ns/op 0 B/op 0 allocs/op 43 | BenchmarkFuseTriggerOnly_2Waiters-4 500000 645 ns/op 0 B/op 0 allocs/op 44 | BenchmarkFuseTriggerOnly_4Waiters-4 300000 1373 ns/op 0 B/op 0 allocs/op 45 | BenchmarkFuseTriggerOnly_8Waiters-4 100000 2747 ns/op 0 B/op 0 allocs/op 46 | 47 | Cautions: 48 | 49 | - The `BenchmarkLatchTrigger_*Listeners` family uses unbuffered channels, 50 | because we don't want to start measuring the vageries of goroutine scheduling. 51 | - The json "canary" test is phased by way more weird stuff than you'd like to think. 52 | - The amount of GC work created by the first test phases it (yes, 53 | regardless of the benchmark framework's attempt to compensate for that). 54 | - Bizarrely, maxprocs affects the json canary more than any other test. 55 | 56 | Observations: 57 | 58 | - Setting one listener costs about half (or less) as much as a small json parse. 59 | - Subscribing gatherer chans to the latch is O(n) (no surprise there). 60 | - ~700ns per additional gatherer 61 | - Triggering the latch is O(n) in the gatherer count (no surprise there). 62 | - ~62-68ns per additional buffered gatherer to signal with no blocking receiver; ~200ns baseline -- per BufDeadend tests 63 | - ~340-420ns per additional unbuffered gatherer to signal which taps the scheduler; ~200ns basline -- per UnbufGather test 64 | - Closing an empty/signal channel is O(n) in the blocked reader count! 65 | - ~290-340ns per additional blocked reader 66 | - (not shown) Additional reads after the block returns are so cheap they're immeasurable (no suprise there). 67 | - (not shown) Changing the fuse chan to buffered size=1 has no impact (no surprise there; it's still blocking-or-not for the reader). 68 | - Comparing the previous two points: 69 | - *Scheduling* is the biggest cost incurred; it's approx 300ns on these tests. 70 | Whether or not there's a blocked reader significantly predominates other factors. 71 | - `close` has essentially no other overhead, being a builtin (I presume). 72 | - Using lists of channels for the gatherer pattern heaps another 70ns or thereabout onto the costs, mostly for the lock. 73 | - Remember, comparing these on costs is academic; they fundamentally don't have the same blocking patterns. 74 | - Remember, the scale of these costs are ringing in at 1/10th of the cost of a 28-char json unmarshal. 75 | */ 76 | 77 | func BenchmarkLatchAllocation(b *testing.B) { 78 | /* 79 | A quick word about the cost of allocations in microbenchmarks like this: 80 | THEY MATTER. 81 | 82 | There are approximately three ways you can write this: 83 | 84 | 1. `malloc := make([]Latch, b.N); b.ResetTimer()` 85 | 2. that, but skip the reset 86 | 3. `_ = New()` 87 | 4. `var x Latch`, assign in the loop, and then sink the "unused" into `_ = x` 88 | 89 | On my machine: 90 | 91 | 1. 348 ns 92 | 2. 359 ns -- about 3%; small, but consistent 93 | 3. 142 ns -- the compiler is optimizing things out! 94 | 4. 326 ns -- about 9% faster than discounted prealloc; just indexing in costs that much. 95 | 96 | So! While 1 and 4 are valid, 2 and 3 are *not*; and if you're 97 | building benchmark functions to compare against each other, they 98 | must consistently choose either strategy 1 or consistently strategy 4, 99 | or they will not be comparable apples-to-apples. 100 | 101 | Note that `StopTimer` and `StartTimer` can NOT solve these issues 102 | unless they're well above a certain timescale, and even then are 103 | rather remarkably costly in terms of wall clock run time your 104 | benchmark will now require. If this `x = New()` is flanked in 105 | start/stop, the benchmark *still* reports 63.4 ns -- hugely out of 106 | line; removing the loop body results in 0.94 ns (as a no-op should)! 107 | Therefore, removing things from the loop body entirely remains 108 | your only safe option for anything measured in nanos. 109 | Meanwhile, running with start/stops makes wall clock timme jump from 110 | 2sec to over 100 sec (!), because of the overhead the benchmark system 111 | sinks into gathering memory stats in every toggle. 112 | 113 | Here, we had to go a step further, because of two competing influences: 114 | the test itself is short, so go bench will run our `b.N` sky high; 115 | and yet our memory usage will get ridiculous at that `N` and start 116 | to have other difficult-to-constrain deleterious effects. 117 | This shouldn't be a common problem; it's most likely a sign of a 118 | badly targetted benchmark (and this is; it's illustrative only). 119 | (See git history for an exploration of how memory pressure had a 120 | *crushing* effect on a *subsequent* benchmark function! This is a 121 | situation to avoid at all costs.) 122 | */ 123 | subbatch(b, func(b *testing.B) { 124 | b.StopTimer() 125 | latchPool := make([]Latch, b.N) 126 | b.StartTimer() 127 | for i := 0; i < b.N; i++ { 128 | latchPool[i] = New() 129 | } 130 | }) 131 | } 132 | 133 | // Totally unrelated. Just to put things in context. 134 | func BenchmarkBaseline_JsonUnmarshalling(b *testing.B) { 135 | subbatch(b, func(b *testing.B) { 136 | for i := 0; i < b.N; i++ { 137 | var x interface{} 138 | json.Unmarshal([]byte(`{"jsonmsg":{"baseline":42}}`), x) 139 | } 140 | }) 141 | } 142 | 143 | /* 144 | Target: the cost of *triggering*; no one is actually recieving, 145 | (the message just goes into a buffer -- the scheduler will NOT be tapped). 146 | 147 | Not: 148 | - allocating the latch 149 | - allocating the gather chans 150 | - signing up the gather chans 151 | - receiving the event (it goes into the chan buffer) 152 | */ 153 | func DoBenchmkLatchTriggerOnly_NBufDeadend(b *testing.B, n int) { 154 | subbatch(b, func(b *testing.B) { 155 | b.StopTimer() 156 | latchPool := make([]Latch, b.N) 157 | for i := 0; i < b.N; i++ { 158 | x := New() 159 | for j := 0; j < n; j++ { 160 | x.WaitSelectably(make(chan interface{}, 1)) 161 | } 162 | latchPool[i] = x 163 | } 164 | b.StartTimer() 165 | for i := 0; i < b.N; i++ { 166 | latchPool[i].Trigger() 167 | } 168 | }) 169 | } 170 | func BenchmarkLatchTriggerOnly_0BufDeadend(b *testing.B) { DoBenchmkLatchTriggerOnly_NBufDeadend(b, 0) } 171 | func BenchmarkLatchTriggerOnly_1BufDeadend(b *testing.B) { DoBenchmkLatchTriggerOnly_NBufDeadend(b, 1) } 172 | func BenchmarkLatchTriggerOnly_2BufDeadend(b *testing.B) { DoBenchmkLatchTriggerOnly_NBufDeadend(b, 2) } 173 | func BenchmarkLatchTriggerOnly_4BufDeadend(b *testing.B) { DoBenchmkLatchTriggerOnly_NBufDeadend(b, 4) } 174 | func BenchmarkLatchTriggerOnly_8BufDeadend(b *testing.B) { DoBenchmkLatchTriggerOnly_NBufDeadend(b, 8) } 175 | 176 | /* 177 | Target: the cost of *triggering*, now with someone receiving 178 | (immediately ready, but unbuffered -- so the scheduler will be tapped). 179 | 180 | Not: 181 | - allocating the latch 182 | - allocating the gather chans 183 | - signing up the gather chans 184 | - receiving the event -- someone does block for it, 185 | but we don't await them. 186 | */ 187 | func DoBenchmkLatchTriggerOnly_NUnbufGather(b *testing.B, n int) { 188 | subbatch(b, func(b *testing.B) { 189 | b.StopTimer() 190 | latchPool := make([]Latch, b.N) 191 | for i := 0; i < b.N; i++ { 192 | x := New() 193 | for j := 0; j < n; j++ { 194 | ch := make(chan interface{}, 1) 195 | x.WaitSelectably(ch) 196 | go func() { <-ch }() 197 | } 198 | latchPool[i] = x 199 | } 200 | b.StartTimer() 201 | for i := 0; i < b.N; i++ { 202 | latchPool[i].Trigger() 203 | } 204 | }) 205 | } 206 | func BenchmarkLatchTriggerOnly_0UnbufGather(b *testing.B) { 207 | DoBenchmkLatchTriggerOnly_NUnbufGather(b, 0) 208 | } 209 | func BenchmarkLatchTriggerOnly_1UnbufGather(b *testing.B) { 210 | DoBenchmkLatchTriggerOnly_NUnbufGather(b, 1) 211 | } 212 | func BenchmarkLatchTriggerOnly_2UnbufGather(b *testing.B) { 213 | DoBenchmkLatchTriggerOnly_NUnbufGather(b, 2) 214 | } 215 | func BenchmarkLatchTriggerOnly_4UnbufGather(b *testing.B) { 216 | DoBenchmkLatchTriggerOnly_NUnbufGather(b, 4) 217 | } 218 | func BenchmarkLatchTriggerOnly_8UnbufGather(b *testing.B) { 219 | DoBenchmkLatchTriggerOnly_NUnbufGather(b, 8) 220 | } 221 | 222 | /* 223 | Target: the cost of allocating a new chan and subscribing it. 224 | 225 | Not: 226 | - allocating the latch 227 | - triggering the latch 228 | - receiving the event (it goes into the chan buffer) 229 | 230 | Note: you don't wanna do this one with zero gatherers, because it's 231 | basically testing a noop but doing so in a way that hammers pause button 232 | and thus wastes a ton of wall clock time on memory stats that don't matter. 233 | 234 | Note: all these are subscribes before the trigger; none after. 235 | There is no test for the post-trigger subscribes. 236 | It's not clear what use this would be, because they essentially hit 237 | the same lock mechanism (for that matter, most of what we're testing 238 | here with increasing chan counts is the chan alloc, and then an 239 | `append` call inside the latch; the lock is also all the same here). 240 | */ 241 | func DoBenchmkLatchSubscribe_NBufDeadend(b *testing.B, n int) { 242 | subbatch(b, func(b *testing.B) { 243 | b.StopTimer() 244 | latchPool := make([]Latch, b.N) 245 | for i := 0; i < b.N; i++ { 246 | latchPool[i] = New() 247 | } 248 | b.StartTimer() 249 | for i := 0; i < b.N; i++ { 250 | x := latchPool[i] 251 | for j := 0; j < n; j++ { 252 | x.WaitSelectably(make(chan interface{}, 1)) 253 | } 254 | } 255 | b.StopTimer() 256 | for i := 0; i < b.N; i++ { 257 | latchPool[i].Trigger() 258 | } 259 | b.StartTimer() 260 | }) 261 | } 262 | func BenchmarkLatchSubscribe_1BufDeadend(b *testing.B) { DoBenchmkLatchSubscribe_NBufDeadend(b, 1) } 263 | func BenchmarkLatchSubscribe_2BufDeadend(b *testing.B) { DoBenchmkLatchSubscribe_NBufDeadend(b, 2) } 264 | func BenchmarkLatchSubscribe_4BufDeadend(b *testing.B) { DoBenchmkLatchSubscribe_NBufDeadend(b, 4) } 265 | func BenchmarkLatchSubscribe_8BufDeadend(b *testing.B) { DoBenchmkLatchSubscribe_NBufDeadend(b, 8) } 266 | 267 | /* 268 | Target: the group cost of allocating chans, subscribing them, and triggering. 269 | 270 | This should be approximately the sum of the subscribe and trigger tests, 271 | if all in the world adds up nicely. 272 | */ 273 | func DoBenchmkLatchFullCycle_NBufDeadend(b *testing.B, n int) { 274 | subbatch(b, func(b *testing.B) { 275 | b.StopTimer() 276 | latchPool := make([]Latch, b.N) 277 | for i := 0; i < b.N; i++ { 278 | latchPool[i] = New() 279 | } 280 | b.StartTimer() 281 | for i := 0; i < b.N; i++ { 282 | x := latchPool[i] 283 | for j := 0; j < n; j++ { 284 | x.WaitSelectably(make(chan interface{}, 1)) 285 | } 286 | x.Trigger() 287 | } 288 | }) 289 | } 290 | func BenchmarkLatchFullCycle_0BufDeadend(b *testing.B) { DoBenchmkLatchFullCycle_NBufDeadend(b, 0) } 291 | func BenchmarkLatchFullCycle_1BufDeadend(b *testing.B) { DoBenchmkLatchFullCycle_NBufDeadend(b, 1) } 292 | func BenchmarkLatchFullCycle_2BufDeadend(b *testing.B) { DoBenchmkLatchFullCycle_NBufDeadend(b, 2) } 293 | func BenchmarkLatchFullCycle_4BufDeadend(b *testing.B) { DoBenchmkLatchFullCycle_NBufDeadend(b, 4) } 294 | func BenchmarkLatchFullCycle_8BufDeadend(b *testing.B) { DoBenchmkLatchFullCycle_NBufDeadend(b, 8) } 295 | 296 | /* 297 | Target: the cost of *triggering*. 298 | 299 | We spawn $N goroutines to each block reading on the fuse channel. 300 | There's matching buffered/no-blocker test because there's no other way to "subscribe". 301 | This is most comparable to the unbuffered gather chans with blocked readers 302 | in the the other latch tests. 303 | 304 | Not: 305 | - allocating the latch 306 | - blocking for the signals 307 | */ 308 | func DoBenchmkFuseTriggerOnly_NWaiters(b *testing.B, n int) { 309 | subbatch(b, func(b *testing.B) { 310 | b.StopTimer() 311 | fusePool := make([]*fuse, b.N) 312 | for i := 0; i < b.N; i++ { 313 | x := NewFuse() 314 | for j := 0; j < n; j++ { 315 | go func() { <-x.Selectable() }() 316 | } 317 | fusePool[i] = x 318 | } 319 | b.StartTimer() 320 | for i := 0; i < b.N; i++ { 321 | fusePool[i].Fire() 322 | } 323 | }) 324 | } 325 | func BenchmarkFuseTriggerOnly_0Waiters(b *testing.B) { DoBenchmkFuseTriggerOnly_NWaiters(b, 0) } 326 | func BenchmarkFuseTriggerOnly_1Waiters(b *testing.B) { DoBenchmkFuseTriggerOnly_NWaiters(b, 1) } 327 | func BenchmarkFuseTriggerOnly_2Waiters(b *testing.B) { DoBenchmkFuseTriggerOnly_NWaiters(b, 2) } 328 | func BenchmarkFuseTriggerOnly_4Waiters(b *testing.B) { DoBenchmkFuseTriggerOnly_NWaiters(b, 4) } 329 | func BenchmarkFuseTriggerOnly_8Waiters(b *testing.B) { DoBenchmkFuseTriggerOnly_NWaiters(b, 8) } 330 | -------------------------------------------------------------------------------- /latch/testutil_test.go: -------------------------------------------------------------------------------- 1 | package latch 2 | 3 | import "testing" 4 | 5 | func subbatch(b *testing.B, fn func(*testing.B)) { 6 | maxSize := 100 * 1000 7 | originalN := b.N 8 | for n := b.N; n > 0; n -= maxSize { 9 | b.N = n // max left 10 | if b.N > maxSize { 11 | b.N = maxSize 12 | } 13 | fn(b) 14 | } 15 | b.N = originalN 16 | } 17 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | type WritName []string 10 | 11 | func (wn WritName) String() string { 12 | if len(wn) == 0 { 13 | return "[root]" 14 | } 15 | return strings.Join(wn, ".") 16 | } 17 | 18 | func (wn WritName) Coda() string { 19 | if len(wn) == 0 { 20 | return "[root]" 21 | } 22 | return wn[len(wn)-1] 23 | } 24 | 25 | func (wn WritName) New(segment string) WritName { 26 | result := make([]string, len(wn)+1) 27 | copy(result, wn) 28 | result[len(wn)] = segment 29 | return result 30 | } 31 | 32 | /* 33 | Called to log lifecycle events inside the supervision system. 34 | 35 | An example event might be 36 | 37 | log(mgr.FullName, "child reaped", writ.Name) 38 | 39 | which one might log as, for example: 40 | 41 | log.debug(evt, {"mgr":name, "regarding":re.Coda()}) 42 | //debug: child reaped -- mgr=root.system.subsys regarding=subproc14 43 | 44 | The `name` and `evt` parameters will always be nonzero; `re` is optional. 45 | The `important` parameter will be true if this should *really* be printed; 46 | "important" events are low-volume things and generally warnings, like 47 | the warnings printed for agents that are not responding to quit signals. 48 | */ 49 | type LogFn func(name WritName, evt string, re WritName, important bool) 50 | 51 | var log LogFn = func(name WritName, evt string, re WritName, _ bool) { 52 | if re == nil { 53 | fmt.Fprintf(os.Stderr, "mgr=%s: %q\n", name, evt) 54 | } else { 55 | fmt.Fprintf(os.Stderr, "mgr=%s: %q re=%s\n", name, evt, re.Coda()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /logging_test.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestLogging(t *testing.T) { 10 | Convey("WritNames should dtrt", t, func() { 11 | wn := &WritName{} 12 | 13 | Convey("Empty writnames serialize specially", func() { 14 | So(wn.String(), ShouldResemble, "[root]") 15 | So(wn.Coda(), ShouldResemble, "[root]") 16 | }) 17 | 18 | Convey("Appending a segment works", func() { 19 | wn1 := wn.New("sys") 20 | So(wn1.String(), ShouldResemble, "sys") 21 | So(wn1.Coda(), ShouldResemble, "sys") 22 | 23 | Convey("Parent writnames was not modified", func() { 24 | So(wn.String(), ShouldResemble, "[root]") 25 | So(wn.Coda(), ShouldResemble, "[root]") 26 | }) 27 | 28 | Convey("Chains of segment work", func() { 29 | wn2 := wn.New("laser").New("bank1") 30 | So(wn2.String(), ShouldResemble, "laser.bank1") 31 | So(wn2.Coda(), ShouldResemble, "bank1") 32 | }) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "go.polydawn.net/go-sup/latch" 9 | "go.polydawn.net/go-sup/sluice" 10 | ) 11 | 12 | type manager struct { 13 | reportingTo Supervisor // configured at start 14 | 15 | ctrlChan_winddown latch.Fuse // set at init. fired by external event. 16 | ctrlChan_quit latch.Fuse // set at init. fired by external event. 17 | doneFuse latch.Fuse // set at init. fired to announce internal state change. 18 | 19 | mu sync.Mutex // must hold while touching wards 20 | accepting bool // must hold `mu`. if false, may no longer append to wards. 21 | wards map[Writ]func() // live writs -> cancelfunc 22 | ctrlChan_childDone chan Writ // writs report here when done 23 | tombstones sluice.Sluice // of `Writ`s that are done and not yet externally ack'd. no sync needed. 24 | } 25 | 26 | type ( 27 | reqWrit struct { 28 | name string 29 | ret chan<- Writ 30 | } 31 | ) 32 | 33 | func newManager(reportingTo Supervisor) Manager { 34 | mgr := &manager{ 35 | reportingTo: reportingTo, 36 | 37 | ctrlChan_winddown: latch.NewFuse(), 38 | ctrlChan_quit: latch.NewFuse(), 39 | doneFuse: latch.NewFuse(), 40 | 41 | accepting: true, 42 | wards: make(map[Writ]func()), 43 | ctrlChan_childDone: make(chan Writ), 44 | tombstones: sluice.New(), 45 | } 46 | go mgr.run() 47 | return mgr 48 | } 49 | 50 | func (mgr *manager) NewTask(name string) Writ { 51 | return mgr.releaseWrit(name) 52 | } 53 | 54 | /* 55 | "probably what you want" to do after launching all tasks to get your 56 | management tree to wind up nice. 57 | 58 | - Moves the manager to winddown mode (no more new tasks will be accepted). 59 | - Starts gathering child statuses... 60 | - If any are errors... 61 | - Moves the manager to quit mode (quit signals are sent to all other children). 62 | - Saves that error 63 | - Keeps waiting 64 | - When all children are done... 65 | - Panic up the first error we gathered. (The rest are lost.) 66 | */ 67 | func (mgr *manager) Work() { 68 | mgr.ctrlChan_winddown.Fire() 69 | var devastation error 70 | 71 | // While we're in the winddown state -- 72 | // Passively collecting results, and jump ourselves to quit in case of errors. 73 | PreDoneLoop: 74 | for { 75 | // note: if we had a true fire-drill exit mode, we'd probably 76 | // have a select over `<-mgr.reportingTo.QuitCh()` here as well. 77 | // but we don't really believe in that: cleanup is important. 78 | select { 79 | case rcv := <-mgr.tombstones.Next(): 80 | writ := (rcv).(*writ) 81 | if writ.err != nil { 82 | msg := fmt.Sprintf("manager autoquitting because of error child error: %s", writ.err) 83 | log(mgr.reportingTo.Name(), msg, writ.name, false) 84 | devastation = writ.err 85 | mgr.ctrlChan_quit.Fire() 86 | break PreDoneLoop 87 | } 88 | case <-mgr.doneFuse.Selectable(): 89 | break PreDoneLoop 90 | } 91 | } 92 | 93 | // If we need to keep waiting for alldone, we also tick during it, so 94 | // we can warn you about children not responding to quit reasonably quickly. 95 | quitTime := time.Now() 96 | tick := time.NewTicker(2 * time.Second) 97 | YUNoDoneLoop: 98 | for { 99 | select { 100 | case <-tick.C: 101 | mgr.mu.Lock() 102 | var names []string 103 | for ward, _ := range mgr.wards { 104 | names = append(names, ward.Name().Coda()) 105 | if len(names) > 6 { 106 | names = append(names, "...") 107 | break 108 | } 109 | } 110 | msg := fmt.Sprintf("quit %d ago, still waiting for children: %d remaining [%s]", 111 | int(time.Now().Sub(quitTime).Seconds()), 112 | len(mgr.wards), 113 | names, 114 | ) 115 | mgr.mu.Unlock() 116 | log(mgr.reportingTo.Name(), msg, nil, true) 117 | case <-mgr.doneFuse.Selectable(): 118 | break YUNoDoneLoop 119 | } 120 | } 121 | tick.Stop() 122 | 123 | // Now that we're fully done: range over all the child tombstones, so that 124 | // any errors can be raised upstream (or if we already have a little 125 | // bundle of joy, at least make brief mention of others in the log). 126 | FinalizeLoop: 127 | for { 128 | select { 129 | case rcv := <-mgr.tombstones.Next(): 130 | writ := (rcv).(*writ) 131 | if writ.err != nil { 132 | if devastation != nil { 133 | msg := fmt.Sprintf("manager gathered additional errors while shutting down: %s", writ.err) 134 | log(mgr.reportingTo.Name(), msg, writ.name, true) 135 | continue 136 | } 137 | msg := fmt.Sprintf("manager gathered an error while shutting down: %s", writ.err) 138 | log(mgr.reportingTo.Name(), msg, writ.name, true) 139 | devastation = writ.err 140 | } 141 | default: 142 | // no new tombstones should be coming, so the first time 143 | // polling it blocks, we're done: leave. 144 | break FinalizeLoop 145 | } 146 | } 147 | 148 | // If we collected a child error at any point, raise it now. 149 | if devastation != nil { 150 | panic(devastation) 151 | } 152 | } 153 | 154 | func (mgr *manager) GatherChild() <-chan sluice.T { 155 | return mgr.tombstones.Next() 156 | } 157 | -------------------------------------------------------------------------------- /manager_internal.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | // this file contains the state machine functions for the inner workings of manager 4 | 5 | /* 6 | The maintainence actor. 7 | 8 | Your controller strategy code is running in another goroutine. This one 9 | is in charge of operations like collecting child status, and is 10 | purely internal so it can reliably handle its own blocking behavior. 11 | */ 12 | func (mgr *manager) run() { 13 | log(mgr.reportingTo.Name(), "working", nil, false) 14 | defer log(mgr.reportingTo.Name(), "all done", nil, false) 15 | stepFn := mgr.step_Accepting 16 | for { 17 | if stepFn == nil { 18 | break 19 | } 20 | stepFn = stepFn() 21 | } 22 | } 23 | 24 | /* 25 | Steps in the state machine of the supervisor's internal maint. 26 | 27 | This pattern is awfully nice: 28 | - you can see the transitions by clear name (returns highlight them) 29 | - you *don't* see the visual clutter of code for transitions that are 30 | not possible for whatever state you're currently looking at 31 | - even if things really go poorly, your stack trace clearly indicates 32 | exactly which state you were in (it's in the function name after all). 33 | */ 34 | type mgr_step func() mgr_step 35 | 36 | /* 37 | During Accepting, new requests for writs will be accepted freely. 38 | This function gathers childDone signals and waits for quit or winddown instructions. 39 | */ 40 | func (mgr *manager) step_Accepting() mgr_step { 41 | select { 42 | case childDone := <-mgr.ctrlChan_childDone: 43 | mgr.reapChild(childDone) 44 | return mgr.step_Accepting 45 | 46 | case <-mgr.ctrlChan_quit.Selectable(): 47 | mgr.stopAccepting() 48 | mgr.cancelAll() 49 | return mgr.step_Quitting 50 | case <-mgr.reportingTo.QuitCh(): 51 | mgr.stopAccepting() 52 | mgr.cancelAll() 53 | return mgr.step_Quitting 54 | 55 | case <-mgr.ctrlChan_winddown.Selectable(): 56 | mgr.stopAccepting() 57 | return mgr.step_Winddown 58 | } 59 | } 60 | 61 | /* 62 | During Winddown, new requests for writs will be handled, but immediately rejected. 63 | Winddown is reached either by setting the manager to accept no new work. 64 | Winddown loops until all wards have been gathered, 65 | then we make the final transition: to terminated. 66 | Quits during winddown take us to the Quitting phase, which is mostly 67 | identical except for obvious reasons doesn't have to keep waiting for 68 | the potential of a quit signal. 69 | */ 70 | func (mgr *manager) step_Winddown() mgr_step { 71 | if len(mgr.wards) == 0 { 72 | return mgr.step_Terminated 73 | } 74 | 75 | select { 76 | case childDone := <-mgr.ctrlChan_childDone: 77 | mgr.reapChild(childDone) 78 | return mgr.step_Winddown 79 | 80 | case <-mgr.ctrlChan_quit.Selectable(): 81 | mgr.cancelAll() 82 | return mgr.step_Quitting 83 | case <-mgr.reportingTo.QuitCh(): 84 | mgr.cancelAll() 85 | return mgr.step_Quitting 86 | } 87 | } 88 | 89 | /* 90 | During Quitting, behavior is about as per Winddown, but we've also... 91 | well, quit. 92 | There's no significant difference to this phase, other than that we no 93 | long select on either the winddown or quit transitions. 94 | */ 95 | func (mgr *manager) step_Quitting() mgr_step { 96 | if len(mgr.wards) == 0 { 97 | return mgr.step_Terminated 98 | } 99 | 100 | select { 101 | case childDone := <-mgr.ctrlChan_childDone: 102 | mgr.reapChild(childDone) 103 | return mgr.step_Quitting 104 | } 105 | } 106 | 107 | /* 108 | During Termination, we do some final housekeeping real quick, signaling 109 | our completion and then... 110 | that's it. 111 | */ 112 | func (mgr *manager) step_Terminated() mgr_step { 113 | // Let others see us as done. yayy! 114 | mgr.doneFuse.Fire() 115 | // We've finally stopped selecting. We're done. We're out. 116 | // No other goroutines alive should have reach to this channel, so we can close it. 117 | close(mgr.ctrlChan_childDone) 118 | // It's over. No more step functions to call. 119 | return nil 120 | } 121 | 122 | //// actions 123 | 124 | /* 125 | Release a new writ, appending to wards -- or, if in any state other 126 | than accepting, return a thunk implement writ but rejecting any work. 127 | 128 | This is the only action that can be called from outside the maint actor. 129 | */ 130 | func (mgr *manager) releaseWrit(name string) Writ { 131 | mgr.mu.Lock() 132 | defer mgr.mu.Unlock() 133 | // No matter what, we're responding, and it earns a name. 134 | writName := mgr.reportingTo.Name().New(name) 135 | // If outside of the accepting states, reject by responding with a doa writ. 136 | if !mgr.accepting { 137 | log(mgr.reportingTo.Name(), "manager rejected writ requisition", writName, false) 138 | // Send back an unusable monad. 139 | return &writ{ 140 | name: writName, 141 | phase: int32(WritPhase_Terminal), 142 | } 143 | } 144 | // Ok, we're doing it: make a new writ to track this upcoming task. 145 | log(mgr.reportingTo.Name(), "manager releasing writ", writName, false) 146 | wrt := newWrit(writName) 147 | // Assign our final report hook to call back home. 148 | wrt.afterward = func() { 149 | log(mgr.reportingTo.Name(), "writ turning in", writName, false) 150 | mgr.ctrlChan_childDone <- wrt 151 | } 152 | // Register it. 153 | mgr.wards[wrt] = wrt.quitFuse.Fire 154 | // Release it into the wild. 155 | return wrt 156 | } 157 | 158 | func (mgr *manager) stopAccepting() { 159 | mgr.mu.Lock() 160 | mgr.accepting = false 161 | mgr.mu.Unlock() 162 | } 163 | 164 | func (mgr *manager) reapChild(childDone Writ) { 165 | mgr.mu.Lock() 166 | defer mgr.mu.Unlock() 167 | log(mgr.reportingTo.Name(), "reaped child", childDone.Name(), false) 168 | delete(mgr.wards, childDone) 169 | mgr.tombstones.Push(childDone) 170 | } 171 | 172 | func (mgr *manager) cancelAll() { 173 | mgr.mu.Lock() 174 | defer mgr.mu.Unlock() 175 | log(mgr.reportingTo.Name(), "manager told to cancel all!", nil, false) 176 | for _, cancelFn := range mgr.wards { 177 | cancelFn() 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /manager_test.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestManager(t *testing.T) { 12 | Convey("Given a Manager", t, func() { 13 | rootWrit := NewTask() 14 | rootWrit.Run(func(super Supervisor) { 15 | mgr := NewManager(super) 16 | 17 | Convey("And some serially executing tasks", func() { 18 | ch := make(chan string, 2) 19 | mgr.NewTask("1").Run(ChanWriterAgent("1", ch)) 20 | mgr.NewTask("2").Run(ChanWriterAgent("2", ch)) 21 | 22 | Convey("Tasks run and we see their sideeffects", func() { 23 | So(<-ch, ShouldEqual, "1") 24 | So(<-ch, ShouldEqual, "2") 25 | }) 26 | 27 | Convey("Manager.Work should gather all the things", func() { 28 | mgr.Work() 29 | // maybe not the most useful test 30 | So(mgr.(*manager).doneFuse.IsBlown(), ShouldBeTrue) 31 | So(mgr.(*manager).wards, ShouldHaveLength, 0) 32 | }) 33 | }) 34 | 35 | Convey("And some parallel executing tasks", func() { 36 | ch := make(chan string, 2) 37 | go mgr.NewTask("1").Run(ChanWriterAgent("1", ch)) 38 | go mgr.NewTask("2").Run(ChanWriterAgent("2", ch)) 39 | 40 | Convey("Tasks run and we see their sideeffects", func() { 41 | results := []string{<-ch, <-ch} 42 | sort.Strings(results) 43 | So(results[0], ShouldEqual, "1") 44 | So(results[1], ShouldEqual, "2") 45 | }) 46 | 47 | Convey("Manager.Work should gather all the things", func() { 48 | mgr.Work() 49 | // maybe not the most useful test 50 | So(mgr.(*manager).doneFuse.IsBlown(), ShouldBeTrue) 51 | So(mgr.(*manager).wards, ShouldHaveLength, 0) 52 | }) 53 | }) 54 | 55 | Convey("And some exploding tasks!", func() { 56 | ch := make(chan string, 0) 57 | explo := fmt.Errorf("bang!") 58 | go mgr.NewTask("1").Run(ChanWriterAgent("1", ch)) 59 | go mgr.NewTask("2").Run(ChanWriterAgent("2", ch)) 60 | go mgr.NewTask("e").Run(ExplosiveAgent(explo)) 61 | 62 | Convey("Manager.Work should raise the panic", func() { 63 | So(mgr.Work, ShouldPanic) 64 | So(mgr.(*manager).doneFuse.IsBlown(), ShouldBeTrue) 65 | So(mgr.(*manager).wards, ShouldHaveLength, 0) 66 | }) 67 | }) 68 | }) 69 | }) 70 | } 71 | 72 | func ChanWriterAgent(msg string, ch chan<- string) Agent { 73 | return func(supvr Supervisor) { 74 | select { 75 | case ch <- msg: 76 | case <-supvr.QuitCh(): 77 | } 78 | } 79 | } 80 | 81 | func ExplosiveAgent(err error) Agent { 82 | return func(Supervisor) { 83 | panic(err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /phist/phist.go: -------------------------------------------------------------------------------- 1 | package phist 2 | 3 | import "fmt" 4 | 5 | /* 6 | 7 | Any slice which contains the elements in correct sequence passes: 8 | 9 | So( 10 | []{"a", "c", "b"}, 11 | ShouldSequence, 12 | "a", "b", 13 | ) => true 14 | 15 | Elements in an incorrect order fail: 16 | 17 | So( 18 | []{"b", "a", "c"}, 19 | ShouldSequence, 20 | "a", "b", 21 | ) => false 22 | 23 | When elements recur, as long as they could be pairwise ordered, they pass: 24 | 25 | So( 26 | []{"a", "c", "a", "b", "b"}, 27 | ShouldSequence, 28 | "a", "b", 29 | ) => true 30 | 31 | So( 32 | []{"a", "c", "b", "b", "a"}, 33 | ShouldSequence, 34 | "a", "b", 35 | ) => false 36 | 37 | When elements recur, they must have the latter half at least as often 38 | as the first half: 39 | 40 | So( 41 | []{"a", "c", "a", "b", "e"}, 42 | ShouldSequence, 43 | "a", "b", 44 | ) => false 45 | 46 | */ 47 | func ShouldSequence(actual interface{}, expected ...interface{}) string { 48 | // args parsery 49 | sequence, ok := actual.([]string) 50 | if !ok { 51 | return "You must provide a string slice as the first argument to this assertion." 52 | } 53 | var keywords []string 54 | var counts []int 55 | switch len(expected) { 56 | case 0, 1: 57 | return "You must provide at least two parameters as expectations to this assertion." 58 | default: 59 | for _, v := range expected { 60 | keyword, ok := v.(string) 61 | if !ok { 62 | return fmt.Sprintf("You must provide strings as expected values, not %T.", v) 63 | } 64 | keywords = append(keywords, keyword) 65 | counts = append(counts, 0) 66 | } 67 | } 68 | 69 | // run checks against the seq 70 | for i, val := range sequence { 71 | for j, kw := range keywords { 72 | if val == kw { 73 | counts[j]++ 74 | if j == 0 { // first event can always happen again 75 | continue 76 | } 77 | if counts[j-1] < counts[j] { 78 | return fmt.Sprintf("Sequence broken: at index %d: %q occured %d times, overtaking %q which is supposed to precede it but only occured %d", 79 | i, 80 | keywords[j], counts[j], 81 | keywords[j-1], counts[j-1], 82 | ) 83 | } 84 | continue 85 | } 86 | } 87 | } 88 | // at the end, check that there are no more unmatched sequence starts 89 | for i := 1; i < len(counts); i++ { 90 | if counts[i-1] > counts[i] { 91 | return fmt.Sprintf("Sequence broken: at end, %q occured %d times, and %q which is supposed to follow it only occured %d", 92 | keywords[i-1], counts[i-1], 93 | keywords[i], counts[i], 94 | ) 95 | } 96 | } 97 | // also, just not existing is not a valid match 98 | if counts[0] == 0 { 99 | return "Sequence broken: none of the keywords ever encountered" 100 | } 101 | 102 | return "" 103 | } 104 | 105 | /* 106 | Checks that an element occurs $n times: 107 | 108 | So( 109 | []{"a", "c", "b", "c"}, 110 | ShouldOccurWithFrequency, 111 | "c", 2, 112 | ) => true 113 | */ 114 | func ShouldOccurWithFrequency(actual interface{}, expected ...interface{}) string { 115 | return "todo" 116 | } 117 | 118 | /* 119 | Checks that all instances of an element occur before the first incident 120 | of another: 121 | 122 | So( 123 | []{"a", "a", "a", "b"}, 124 | ShouldAllPrecede, 125 | "a", "b", 126 | ) => true 127 | 128 | So( 129 | []{"a", "a", "b", "a"}, 130 | ShouldAllPrecede, 131 | "a", "b", 132 | ) => false 133 | */ 134 | func ShouldAllPrecede(actual interface{}, expected ...interface{}) string { 135 | return "todo" 136 | } 137 | 138 | /* 139 | Same semantics as `ShouldAllPrecede`, in the opposite direction: 140 | 141 | So( 142 | []{"a", "b", "b", "b"}, 143 | ShouldAllFollow, 144 | "b", "a", 145 | ) => true 146 | */ 147 | func ShouldAllFollow(actual interface{}, expected ...interface{}) string { 148 | return "todo" 149 | } 150 | -------------------------------------------------------------------------------- /phist/phist_test.go: -------------------------------------------------------------------------------- 1 | package phist 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func Test(t *testing.T) { 10 | Convey("ShouldSequence should check sequencing", t, func() { 11 | Convey("A slice which contains the elements in correct sequence should pass", func() { 12 | So( 13 | ShouldSequence( 14 | []string{"a", "c", "b"}, 15 | "a", "b", 16 | ), 17 | ShouldEqual, 18 | "", 19 | ) 20 | }) 21 | Convey("Elements in an incorrect order fail", func() { 22 | So( 23 | ShouldSequence( 24 | []string{"b", "a", "c"}, 25 | "a", "b", 26 | ), 27 | ShouldEqual, 28 | `Sequence broken: at index 0: "b" occured 1 times, overtaking "a" which is supposed to precede it but only occured 0`, 29 | ) 30 | }) 31 | Convey("Recurring elements pass as long as they can be pairwise ordered", func() { 32 | So( 33 | ShouldSequence( 34 | []string{"a", "c", "a", "b", "b"}, 35 | "a", "b", 36 | ), 37 | ShouldEqual, 38 | "", 39 | ) 40 | }) 41 | Convey("Recurring elements fail if later parts of the sequence outnumber their precursors", func() { 42 | So( 43 | ShouldSequence( 44 | []string{"a", "c", "b", "b", "a"}, 45 | "a", "b", 46 | ), 47 | ShouldEqual, 48 | `Sequence broken: at index 3: "b" occured 2 times, overtaking "a" which is supposed to precede it but only occured 1`, 49 | ) 50 | }) 51 | Convey("Recurring elements fail if sequences start but lack matches", func() { 52 | So( 53 | ShouldSequence( 54 | []string{"a", "c", "a", "b", "e"}, 55 | "a", "b", 56 | ), 57 | ShouldEqual, 58 | `Sequence broken: at end, "a" occured 2 times, and "b" which is supposed to follow it only occured 1`, 59 | ) 60 | }) 61 | Convey("Lack of relevant elements fails to match", func() { 62 | So( 63 | ShouldSequence( 64 | []string{"a", "c", "a", "b", "e"}, 65 | "f", "g", 66 | ), 67 | ShouldEqual, 68 | `Sequence broken: none of the keywords ever encountered`, 69 | ) 70 | }) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /sluice/sluice.go: -------------------------------------------------------------------------------- 1 | /* 2 | `sluice` provides a way to use channels where you need to select for ready events, 3 | but directly using buffered channels isn't desirable because events need to be 4 | reordered after dropoff; or, because allowing writers to block is simply untenable. 5 | 6 | The read channels produced by a Sluice are buffered and shall eventually 7 | provide one value -- no more; to read again, get another channel. 8 | 9 | A Sluice will internally buffer without limit. 10 | That means if the input volume is unlimited, and the consumers are 11 | slower than the producers, you will eventually run out of memory! 12 | If backpressure is important, a sluice is *not* the right choice; 13 | prefer a regular buffered channel instead. 14 | 15 | Be careful not to create a read request with `Next` and then drop the channel. 16 | Doing so won't crash or deadlock the program, but it will lose a value. 17 | Particularly watch out for this in a select used inside a loop; 18 | naively using `case val := <-sluice.Next():` will spawn a read request 19 | *every* time you enter the select, resulting in too many channels 20 | and lots of lost reads. More correct: keep the reference 21 | and replace it only when you actually follow that select path down. 22 | */ 23 | package sluice 24 | 25 | import ( 26 | "sync" 27 | ) 28 | 29 | type T interface{} 30 | 31 | type Sluice interface { 32 | Push(T) 33 | Next() <-chan T 34 | } 35 | 36 | func New() Sluice { 37 | return &sluice{ 38 | serviceReqs: make(map[chan T]struct{}), 39 | } 40 | } 41 | 42 | type sluice struct { 43 | mu sync.Mutex 44 | serviceReqs map[chan T]struct{} 45 | queue []T 46 | } 47 | 48 | func (db *sluice) Push(x T) { 49 | db.mu.Lock() 50 | defer db.mu.Unlock() 51 | req := db.pluck() 52 | if req == nil { 53 | db.queue = append(db.queue, x) 54 | } else { 55 | req <- x 56 | } 57 | } 58 | 59 | func (db *sluice) pluck() chan T { 60 | for req, _ := range db.serviceReqs { 61 | delete(db.serviceReqs, req) 62 | return req 63 | } 64 | return nil 65 | } 66 | 67 | /* 68 | Request a pull of what's next; a channel for the future result is returned. 69 | One value will eventually be sent on the channel. 70 | */ 71 | func (db *sluice) Next() <-chan T { 72 | respCh := make(chan T, 1) 73 | db.mu.Lock() 74 | defer db.mu.Unlock() 75 | if len(db.queue) > 0 { 76 | var pop T 77 | pop, db.queue = db.queue[0], db.queue[1:] 78 | respCh <- pop 79 | } else { 80 | db.serviceReqs[respCh] = struct{}{} 81 | } 82 | return respCh 83 | } 84 | -------------------------------------------------------------------------------- /sluice/sluice_test.go: -------------------------------------------------------------------------------- 1 | package sluice 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func Test(t *testing.T) { 10 | Convey("Sluice can...", t, func() { 11 | gondola := New() 12 | 13 | Convey("pump values", func() { 14 | gondola.Push("x") 15 | gondola.Push("y") 16 | gondola.Push("z") 17 | So(<-gondola.Next(), ShouldEqual, "x") 18 | So(<-gondola.Next(), ShouldEqual, "y") 19 | So(<-gondola.Next(), ShouldEqual, "z") 20 | }) 21 | 22 | Convey("block when empty", func() { 23 | var answered bool 24 | select { 25 | case <-gondola.Next(): 26 | answered = true 27 | default: 28 | answered = false 29 | } 30 | So(answered, ShouldEqual, false) 31 | 32 | Convey("answers even dropped channels", func() { 33 | gondola.Push("1") 34 | // we still don't expect an answer, 35 | // because the "1" routed to the channel in the prev test. 36 | secondReq := gondola.Next() 37 | select { 38 | case <-secondReq: 39 | answered = true 40 | default: 41 | answered = false 42 | } 43 | So(answered, ShouldEqual, false) 44 | 45 | Convey("definitely answers eventually", func() { 46 | gondola.Push("2") 47 | So(<-secondReq, ShouldEqual, "2") 48 | }) 49 | }) 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /writ.go: -------------------------------------------------------------------------------- 1 | package sup 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | 7 | "go.polydawn.net/meep" 8 | 9 | "go.polydawn.net/go-sup/latch" 10 | ) 11 | 12 | type writ struct { 13 | name WritName 14 | phase int32 15 | quitFuse latch.Fuse // fire this to move to quitting 16 | doneFuse latch.Fuse // we'll fire this when moving to done 17 | svr Supervisor 18 | afterward func() 19 | err error 20 | } 21 | 22 | /* 23 | Phases: 24 | 25 | - Issued 26 | - InUse 27 | - Quitting 28 | - Terminal 29 | 30 | When first created, the phase is 'Issued'. 31 | 32 | When `Run(fn)` is called, if the phase is 'Issued', the phase becomes 'InUse'; 33 | if the phase is 'Terminal', it stays 'Terminal' and `fn` will be ignored. 34 | 35 | When `fn` returns, the phase becomes 'Terminal'. 36 | 37 | When `Cancel` is called, the phase is jumped to 'Quitting' if `fn` is still running; 38 | the phase remains 'Terminal' if `fn` already returned, or if we got there via a previous `Cancel`; 39 | the phase jumps directly to 'Terminal' if `Run(fn)` has not yet been called. 40 | 41 | Note that if you call `Run(fn)` and `Cancel` in parallel, the `fn` may never run. 42 | 43 | If `Run(fn2)` is called a second time for any reason, a panic is raised. 44 | */ 45 | type WritPhase int32 46 | 47 | const ( 48 | WritPhase_Invalid WritPhase = iota 49 | WritPhase_Issued 50 | WritPhase_InUse 51 | WritPhase_Quitting 52 | WritPhase_Terminal 53 | 54 | writFlag_Used WritPhase = 1 << 8 55 | ) 56 | 57 | func newWrit(name WritName) *writ { 58 | quitFuse := latch.NewFuse() 59 | return &writ{ 60 | name: name, 61 | phase: int32(WritPhase_Issued), 62 | quitFuse: quitFuse, 63 | doneFuse: latch.NewFuse(), 64 | svr: &supervisor{name, quitFuse}, 65 | afterward: func() {}, 66 | } 67 | } 68 | 69 | func (writ *writ) Name() WritName { 70 | return writ.name 71 | } 72 | 73 | func (writ *writ) Run(fn Agent) (ret Writ) { 74 | ret = writ 75 | var fly bool 76 | for { 77 | fly = false 78 | ph := WritPhase(atomic.LoadInt32(&writ.phase)) 79 | if ph&writFlag_Used != 0 { 80 | panic("it is not valid to use a writ more than once") 81 | } 82 | var next WritPhase 83 | switch ph { 84 | case WritPhase_Issued: 85 | fly = true 86 | next = WritPhase_InUse 87 | case WritPhase_Terminal: 88 | fly = false 89 | next = WritPhase_Terminal 90 | case WritPhase_InUse, WritPhase_Quitting: 91 | // these statespaces should be unreachable because `writFlag_Used` already covers them. 92 | fallthrough 93 | default: 94 | panic(fmt.Sprintf("invalid writ state %d", ph)) 95 | } 96 | next = next | writFlag_Used 97 | if atomic.CompareAndSwapInt32(&writ.phase, int32(ph), int32(next)) { 98 | break 99 | } 100 | } 101 | if !fly { 102 | // the writ was cancelled before our goroutine really got started; 103 | // we have no choice but to quietly pack it in. 104 | return 105 | } 106 | defer writ.afterward() 107 | meep.Try(func() { 108 | fn(writ.svr) 109 | }, meep.TryPlan{ 110 | {ByType: &ErrTaskPanic{}, Handler: func(e error) { 111 | writ.err = meep.Meep( 112 | &ErrTaskPanic{Task: writ.Name()}, 113 | meep.Cause(e), 114 | meep.NoStack(), 115 | ) 116 | }}, 117 | {CatchAny: true, Handler: func(e error) { 118 | writ.err = meep.Meep( 119 | &ErrTaskPanic{Task: writ.Name()}, 120 | meep.Cause(e), 121 | ) 122 | }}, 123 | }) 124 | for { 125 | ph := WritPhase(atomic.LoadInt32(&writ.phase)) 126 | // transition here is not variable, but filter for sanity check 127 | switch ph & ^writFlag_Used { 128 | case WritPhase_InUse, WritPhase_Quitting: 129 | default: 130 | panic(fmt.Sprintf("invalid writ state %d", ph)) 131 | } 132 | if atomic.CompareAndSwapInt32(&writ.phase, int32(ph), int32(WritPhase_Terminal|writFlag_Used)) { 133 | break 134 | } 135 | } 136 | writ.doneFuse.Fire() 137 | return 138 | } 139 | 140 | func (writ *writ) Cancel() Writ { 141 | writ.quitFuse.Fire() 142 | var terminatedHere bool 143 | for { 144 | terminatedHere = false 145 | ph := WritPhase(atomic.LoadInt32(&writ.phase)) 146 | var next WritPhase 147 | switch ph & ^writFlag_Used { 148 | case WritPhase_Issued: 149 | next = WritPhase_Terminal 150 | // there is no Run defer, so we fire the done fuse ourselves 151 | terminatedHere = true 152 | case WritPhase_InUse: 153 | next = WritPhase_Quitting 154 | case WritPhase_Quitting: 155 | return writ // we're already quitting: the Run defer is responsible for the step to terminal. 156 | case WritPhase_Terminal: 157 | return writ // we're already full halted: great. 158 | default: 159 | panic(fmt.Sprintf("invalid writ state %d", ph)) 160 | } 161 | next = next | (ph & writFlag_Used) 162 | if atomic.CompareAndSwapInt32(&writ.phase, int32(ph), int32(next)) { 163 | break 164 | } 165 | } 166 | if terminatedHere { 167 | writ.doneFuse.Fire() 168 | } 169 | return writ 170 | } 171 | 172 | func (writ *writ) Err() error { 173 | <-writ.doneFuse.Selectable() 174 | return writ.err 175 | } 176 | 177 | func (writ *writ) DoneCh() <-chan struct{} { 178 | return writ.doneFuse.Selectable() 179 | } 180 | 181 | //// 182 | 183 | type supervisor struct { 184 | name WritName 185 | ctrlChan_quit latch.Fuse // typically a copy of the one from the manager. the supervisor is all receiving end. 186 | } 187 | 188 | func (super *supervisor) Name() WritName { 189 | return super.name 190 | } 191 | 192 | func (super *supervisor) QuitCh() <-chan struct{} { 193 | return super.ctrlChan_quit.Selectable() 194 | } 195 | 196 | func (super *supervisor) Quit() bool { 197 | return super.ctrlChan_quit.IsBlown() 198 | } 199 | --------------------------------------------------------------------------------