Skip to content

Commit 60aff86

Browse files
committed
Tried to implement go-auto-reloading.
Whole day of my life wasted: golang/go#20461. Fanfuckingtastic. Fuck you go.
1 parent 0a79837 commit 60aff86

File tree

8 files changed

+493
-110
lines changed

8 files changed

+493
-110
lines changed

doc/manual.org

+2-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ use the Vorlage, not develop for it.
137137
of the life cycle for the document.
138138
5. *Shutdown Phase*: Vorlage unloads all the processors and deloads
139139
all supporting libraries. All connections and requests will be
140-
terminated.
140+
terminated. The processors must shut everything down, and must free all
141+
memory allocated.
141142

142143
* Documents
143144
Documents are UTF-8 encoded files. The text is not canonicalized, so a

doccomp.go

+262-30
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,154 @@ import (
44
vorlageproc "ellem.so/vorlageproc"
55
"fmt"
66
"io"
7+
"reflect"
78
"regexp"
9+
"sync"
810
"sync/atomic"
911
)
1012

1113
var validProcessorName = regexp.MustCompile(`^[a-z0-9_\-]+$`)
1214

15+
// will also delete any processors in any list that have nil
16+
func (c *Compiler) rebuildProcessors() (err error) {
17+
18+
// completely rebuild the processor list
19+
newlist := make([]vorlageproc.Processor, 0, len(c.cprocessors)+len(c.goprocessors))
20+
newlistinfo := make([]vorlageproc.ProcessorInfo, 0, len(c.cprocessors)+len(c.goprocessors))
21+
22+
// testarr will be a list of pointers to structures that implement vorlage.ProcessorInfo
23+
var testarr = make([]interface{}, 0, len(newlist))
24+
addProc := func(arr interface{}) {
25+
lenr := reflect.ValueOf(arr).Len()
26+
for i := 0; i < lenr; i++ {
27+
v := reflect.ValueOf(arr).Index(i)
28+
testarr = append(testarr, v.Interface())
29+
}
30+
}
31+
32+
// arrptr must be a pointer to an array to pointers
33+
removenulls := func(arrptr interface{}) {
34+
arreml := reflect.ValueOf(arrptr).Elem()
35+
lenr := arreml.Len()
36+
replacement := reflect.MakeSlice(arreml.Type(), 0, lenr)
37+
for i := 0; i < lenr; i++ {
38+
v := arreml.Index(i)
39+
if !v.IsNil() {
40+
replacement = reflect.Append(replacement, v)
41+
}
42+
}
43+
t := replacement.Interface()
44+
a := arrptr
45+
c := reflect.ValueOf(arrptr).Elem()
46+
reflect.ValueOf(arrptr).Elem().Set(replacement)
47+
_ = a
48+
_ = c
49+
_ = t
50+
}
51+
52+
// remove null values to clean up
53+
removenulls(&c.cprocessors)
54+
removenulls(&c.goprocessors)
55+
56+
// copy each type of processor into testarr.
57+
// clang / shared object
58+
addProc(c.cprocessors)
59+
addProc(c.goprocessors)
60+
61+
// find any processors that are no longer in testarr but remain in
62+
// c.processors, those need to be removed.
63+
for i := range c.processors {
64+
var j int
65+
for j = 0; j < len(testarr); j++ {
66+
ptr := reflect.ValueOf(c.processors[i]).Pointer()
67+
ptr2 := reflect.ValueOf(testarr[j]).Pointer()
68+
if ptr == ptr2 {
69+
// we still need this one
70+
break
71+
}
72+
}
73+
if j == len(testarr) {
74+
// this address in processors was not found in the upstream copies.
75+
// thus this processor is no longer needed. Shut it down.
76+
ptr := reflect.ValueOf(c.processors[i]).Pointer()
77+
Logger.Alertf("%s (@ %x) is no longer needed, shutting down", c.processorInfos[i].Name, ptr)
78+
err = c.processors[i].Shutdown()
79+
if err != nil {
80+
Logger.Alertf("error returned from shutdown.. this shouldn't happen as it will be ignored: %s", err)
81+
}
82+
}
83+
}
84+
85+
// now carry over old processors and add new ones.
86+
for i := range testarr {
87+
// is this processor loaed in c.processors?
88+
var j int
89+
for j = 0; j < len(c.processors); j++ {
90+
ptr := reflect.ValueOf(c.processors[j]).Pointer()
91+
ptr2 := reflect.ValueOf(testarr[i]).Pointer()
92+
if ptr == ptr2 {
93+
// yup. loaded already carrie it over
94+
newlist = append(newlist, c.processors[j])
95+
newlistinfo = append(newlistinfo, c.processorInfos[j])
96+
break
97+
}
98+
}
99+
if j == len(c.processors) {
100+
// this processor's address was not found in c.processors, put it in
101+
newlist = append(newlist, testarr[i].(vorlageproc.Processor))
102+
info, err := startupproc(newlist[len(newlist)-1])
103+
if err != nil {
104+
return err
105+
}
106+
newlistinfo = append(newlistinfo, info)
107+
}
108+
}
109+
110+
c.processors = newlist
111+
c.processorInfos = newlistinfo
112+
113+
return nil
114+
}
115+
13116
// everything we'd see in both doccomp-http and doccomp-cli and doccomp-pdf
14117
type Compiler struct {
15118

119+
// processors is technically a list of pointers to items found in cprocessors
120+
// and goprocessors.
121+
// If you want to change processors, you must update cprocessors / goprocessors
122+
// and then run updateProcessors()...
16123
// these two arrays are associative
17124
processors []vorlageproc.Processor
18125
processorInfos []vorlageproc.ProcessorInfo
19126

20-
// used for thread safety of shutdown.
127+
// if you change these, you need to run rebuildProcessors to take effect.
128+
// if you set the pointers to nil, they will be marked for deletion.
129+
// if you change the address of the pointer, they will be marked for reload.
130+
cprocessors []*cProc
131+
goprocessors []*goProc
132+
133+
// access these via the atomic.Load... funcitons
21134
concurrentCompiles int64
135+
concurrentReaders int32
136+
137+
// set to anything but 0 to have Compile reject requests.
138+
// 1 = fully shutdown.
139+
// 2 = shutting down but waiting for other compilers to close
140+
// 3 = shutting down but waiting for other readers to close
141+
// 4 = new compiles are being stalled due to a restart/reload of a processor
142+
// the stall will continue until unstall is fed something
143+
//
144+
// shutdownCompilers0 and shutdownReaders0 will be listened to
145+
// by shutdown when shutdown is in 2 and 3 states respectively. will be
146+
// written too. If shutdown not in process, they will be nil
147+
// todo: rename these.
148+
atomicShutdown int32
149+
shutdownCompilers0 chan bool
150+
shutdownReaders0 chan bool
151+
unstall sync.Mutex
152+
153+
// used for watching go reloads if AutoReloadGoFiles
154+
gowatcher *watcher
22155
}
23156

24157
type compileRequest struct {
@@ -81,28 +214,51 @@ func (c compileRequest) String() string {
81214
return str
82215
}
83216

217+
// see https://github.com/golang/go/issues/20461
218+
var AutoReloadGoFiles bool = false
219+
84220
// will return an error if a processor failed to start and/or is invalid
85-
func NewCompiler(proc []vorlageproc.Processor) (c Compiler, err error) {
86-
c.processors = proc
221+
func NewCompiler() (c *Compiler, err error) {
87222

88-
// load all the infos
89-
c.processorInfos = make([]vorlageproc.ProcessorInfo, len(proc))
90-
for i := range c.processors {
91-
c.processorInfos[i], err = c.processors[i].Startup()
92-
Logger.Debugf("starting %s...", c.processorInfos[i].Name)
93-
if err != nil {
94-
Logger.Alertf("processor %s failed to start: %s", c.processorInfos[i].Name, err)
95-
return c, err
96-
}
97-
err = validate(&(c.processorInfos[i]))
98-
if err != nil {
99-
return c, err
223+
// structure set up
224+
c = new(Compiler)
225+
226+
// load the go processors
227+
c.goprocessors, err = loadGoProcessors(GoPluginLoadPath)
228+
if err != nil {
229+
return c, err
230+
}
231+
defer func() {
232+
if AutoReloadGoFiles {
233+
go c.watchGoPath(GoPluginLoadPath)
100234
}
101-
Logger.Infof("loaded processor %s", c.processorInfos[i].Name)
102-
Logger.Debugf("%s information:\n%s", c.processorInfos[i].Name, c.processorInfos[i])
235+
}()
236+
237+
// load the c processors
238+
c.cprocessors, err = loadCProcessors(CLoadPath)
239+
if err != nil {
240+
return c, err
103241
}
104242

105-
return c, nil
243+
return c, c.rebuildProcessors()
244+
}
245+
246+
// helper to rebuildProcessors
247+
func startupproc(proc vorlageproc.Processor) (info vorlageproc.ProcessorInfo, err error) {
248+
ptr := reflect.ValueOf(proc).Pointer()
249+
info, err = proc.Startup()
250+
Logger.Debugf("starting %s (@ %x)...", info.Name, ptr)
251+
if err != nil {
252+
Logger.Alertf("processor %s (@ %x) failed to start: %s", info.Name, ptr, err)
253+
return info, err
254+
}
255+
err = validate(&(info))
256+
if err != nil {
257+
return info, err
258+
}
259+
Logger.Infof("successfully loaded processor %s (@ %x)", info.Name, ptr)
260+
Logger.Debugf("%s information:\n%s", info.Name, info)
261+
return info, err
106262
}
107263

108264
func validate(info *vorlageproc.ProcessorInfo) error {
@@ -180,10 +336,42 @@ type ActionHandler interface {
180336
* Do not attempt to use the streams pointed to by req... they'll be read
181337
* when the docstream is read.
182338
*/
183-
func (comp *Compiler) Compile(filepath string, allInput map[string]string, allStreams map[string]vorlageproc.StreamInput, actionsHandler ActionHandler) (docstream io.ReadCloser, err CompileStatus) {
339+
func (comp *Compiler) Compile(filepath string, allInput map[string]string,
340+
allStreams map[string]vorlageproc.StreamInput, actionsHandler ActionHandler) (docstream io.ReadCloser, err CompileStatus) {
341+
342+
for i := range comp.processors {
343+
Logger.Errorf("%v", comp.processors[i])
344+
}
345+
346+
if shutdowncode := atomic.LoadInt32(&comp.atomicShutdown); shutdowncode != 0 {
347+
var erro error
348+
switch shutdowncode {
349+
case 1:
350+
erro = NewError("compiler has shutdown")
351+
return nil, CompileStatus{erro, false}
352+
case 2:
353+
erro = NewError("compiler is shutting down, waiting on other compiliations to finish")
354+
return nil, CompileStatus{erro, false}
355+
case 3:
356+
erro = NewError("compiler is shutting down, waiting on other readers to finish")
357+
return nil, CompileStatus{erro, false}
358+
case 4:
359+
// if 4, then we will try to lock unstall. which will lock this thread
360+
// until Compiler.cont is called
361+
comp.unstall.Lock()
362+
comp.unstall.Unlock()
363+
}
364+
}
365+
184366
atomic.AddUint64(&nextRid, 1)
185367
atomic.AddInt64(&comp.concurrentCompiles, 1)
186-
defer atomic.AddInt64(&comp.concurrentCompiles, -1)
368+
defer func() {
369+
newi := atomic.AddInt64(&comp.concurrentCompiles, -1)
370+
shutdowncode := atomic.LoadInt32(&comp.atomicShutdown)
371+
if newi == 0 && shutdowncode == 2 {
372+
comp.shutdownCompilers0 <- true
373+
}
374+
}()
187375
compReq := compileRequest{
188376
compiler: comp,
189377
filepath: filepath,
@@ -266,27 +454,71 @@ func (comp *Compiler) Compile(filepath string, allInput map[string]string, allSt
266454
return docstream, CompileStatus{erro, false}
267455
}
268456

269-
return &doc, CompileStatus{}
457+
return doc, CompileStatus{}
270458
}
271459

272460
/*
273461
* Returns all errors that occour when shutting down each processor.
274462
* If there is at least 1 Compile function that has not returned, Shutdown
275463
* will return an error
276464
*/
277-
func (comp *Compiler) Shutdown() []error {
278-
compiles := atomic.LoadInt64(&comp.concurrentCompiles)
279-
if compiles != 0 {
280-
erro := NewError("compiles still running")
281-
erro.SetSubjectf("%d compile compRequest still processing", compiles)
282-
return []error{erro}
465+
func (comp *Compiler) Shutdown() {
466+
if comp.isshutdown() {
467+
return
283468
}
284-
var ret []error
469+
if comp.gowatcher != nil {
470+
comp.gowatcher.close()
471+
}
472+
comp.makestall(1)
473+
474+
// at this point, all readers and compilers are done.
285475
for i := range comp.processors {
286476
err := comp.processors[i].Shutdown()
287477
if err != nil {
288-
ret = append(ret, err)
478+
Logger.Alertf("error returned from shutdown.. this shouldn't happen as it will be ignored: %s", err)
289479
}
290480
}
291-
return ret
481+
}
482+
483+
func (comp *Compiler) isshutdown() bool {
484+
return atomic.LoadInt32(&comp.atomicShutdown) == 1
485+
}
486+
487+
// will wait until all readers and compiles on all threads are complete.
488+
// if code is 1, will cause a full shutdown.
489+
// if code is 4, will stall all Compile calls until cont is called. While stalled,
490+
// you can make changes to the processor
491+
func (comp *Compiler) makestall(code int) {
492+
if comp.isshutdown() {
493+
return
494+
}
495+
comp.shutdownCompilers0 = make(chan bool)
496+
comp.shutdownReaders0 = make(chan bool)
497+
if code == 4 {
498+
Logger.Infof("blocking compiles")
499+
comp.unstall.Lock()
500+
}
501+
defer atomic.StoreInt32(&comp.atomicShutdown, int32(code))
502+
503+
atomic.StoreInt32(&comp.atomicShutdown, 2)
504+
compiles := atomic.LoadInt64(&comp.concurrentCompiles)
505+
if compiles != 0 {
506+
Logger.Infof("waiting for %d compiles to complete...", compiles)
507+
<-comp.shutdownCompilers0
508+
}
509+
510+
atomic.StoreInt32(&comp.atomicShutdown, 3)
511+
readers := atomic.LoadInt32(&comp.concurrentReaders)
512+
if readers != 0 {
513+
Logger.Infof("waiting for %d readers to close...", readers)
514+
<-comp.shutdownReaders0
515+
}
516+
517+
}
518+
519+
// will undo the set-limbo state that makestall made
520+
func (comp *Compiler) cont() {
521+
Logger.Infof("unblocking compiles")
522+
atomic.StoreInt32(&comp.atomicShutdown, 0)
523+
comp.unstall.Unlock()
292524
}

0 commit comments

Comments
 (0)