📜 ⬆️ ⬇️

Simple DICOM client on GO with task balancer and web interface


Hi Habr! Recently, I have been very keen on developing in the GO language. Elegant and expressive programming language. I have long wanted to do something useful. According to the specifics of my work, I have to work with medical archives of DICOM-images PACS.


Github: github.com/Loafter/dtools
Linux-amd64 version: github.com/Loafter/dtools/releases/download/1.0/dcmjsser

I decided it was time to create my dicom-client with (blackjack ..) web interface, which can perform the following standard operations:

(c-echo, c-move, c-store, c-find, respectively).
The GrassRoot SDK library was chosen as dicom-library. Our client will be parallelizing tasks. Go language is well adapted for this.
')
A similar work scenario was described by habrahabr.ru/post/198150 .
Our script is somewhat different:
We have a certain task balancer that receives tasks from a dicom-service, checks the possibility of execution and executes them asynchronously. In order to avoid a situation where 1000 tasks are executed in parallel, we implement the task queue so that there are active tasks and those that are in the sleeping state. By default, only 10 tasks will be active. Otherwise, we could do without a balancer at all, stupidly in parallel to perform 1000 tasks in parallel without any control.
All code balancer is in the file job_ballancer.go.

At the beginning there is a description of handler interfaces. If the work was completed successfully, in the event that the error and the process of processing the task were returned.

type JobDispatcher interface { Dispatch(interface{}) (interface{}, error) } type ErrDispatcher interface { DispatchError(FaJob) error } type CompDispatcher interface { DispatchSuccess(CompJob) error } 


When we create a dispatcher instance, we initialize it with the appropriate handlers.
 srv.jbBal.Init(&srv.dDisp, srv, srv) //   type JobBallancer struct { jChan chan interface{} //      acJob map[string]Job //   slJob map[string]Job //   errDisp ErrDispatcher //   error jobDisp JobDispatcher //  compDisp CompDispatcher //    JbDone sync.WaitGroup //    aJobC int //  () } //  func (jbal *JobBallancer) Init(jdis JobDispatcher, cmd CompDispatcher, erd ErrDispatcher) { jbal.errDisp = erd jbal.jobDisp = jdis jbal.compDisp = cmd jbal.acJob = make(map[string]Job) jbal.slJob = make(map[string]Job) jbal.aJobC = 10 jbal.jChan = make(chan interface{}) go jbal.takeJob() //     //  log.Println("info: job ballancer inited") }     .  , ..    ,   takeJob    . func (jbal *JobBallancer) PushJob(jdat interface{}) error { if jbal.checkInit() { return errors.New("error: JobChan is not inited") } uid := genUid() job := Job{JobId: uid, Data: jdat} jbal.jChan <- job return nil } func (jbal *JobBallancer) takeJob() { for { //    recivedTask := <-jbal.jChan log.Println("info: job taken") switch job := recivedTask.(type) { case TermJob: //         log.Println("info: recive terminate dispatch singal") return case Job: //  (             ) if len(jbal.acJob) < jbal.aJobC { jbal.JbDone.Add(1) jbal.addActiveJob(job) go jbal.startJob(job) log.Println("info: normal dispatch") } else { jbal.addSleepJob(job) jbal.JbDone.Add(1) log.Println("info: attend maximum active job") } case CompJob: //   if err := jbal.compDisp.DispatchSuccess(job); err != nil { log.Println("error: failed dispatch success" + job.Job.JobId) } //       jbal.removeJob(job.Job.JobId) jbal.JbDone.Done() jbal.resumeJobs() case FaJob: //   if err := jbal.errDisp.DispatchError(job); err != nil { log.Println("error: failed dispatch error" + job.Job.JobId) } //      jbal.removeJob(job.Job.JobId) jbal.JbDone.Done() jbal.resumeJobs() default: log.Fatalln("error: unknown job type") jbal.JbDone.Done() } } } //   func (jbal *JobBallancer) removeJob(jid string) error { if _, isFind := jbal.acJob[jid]; isFind { delete(jbal.acJob, jid) } else { return errors.New("error: can't remove job because job with id not found") } return nil } //     ,        ,      func (jbal *JobBallancer) TerminateTakeJob() error { if jbal.checkInit() { return errors.New("error: is not inited") } jbal.JbDone.Wait() jbal.jChan <- TermJob{} close(jbal.jChan) if len(jbal.acJob) > 0 { return errors.New("error: list job is not empty") } log.Println("info: greacefully terminate take job") return nil } 


We will not consider the remaining auxiliary functions. full code can be viewed
github.com/Loafter/dtools/blob/master/dcmjsser/job_ballancer.go

Despite that the code is not complicated and I have been thinking about it for a long time. But still, to test the reliability, I implemented a load test for dozens of tasks:
 testJobDispatcher := TestJobDispatcher{} testErrorDispatcher := TestErrorDispatcher{} testSuccessDispatcher := TestCompletedDispatcher{} jobBallancer := JobBallancer{} jobBallancer.Init(&testJobDispatcher, &testSuccessDispatcher, &testErrorDispatcher) for i := 0; i < 40; i++ { jobBallancer.PushJob("data: " + strconv.Itoa(i)) } jobBallancer.TerminateTakeJob() 

He worked fine. All tasks were completed, and the TerminateTakeJob function ended when all tasks were completed. To control the work done, the sync.WaitGroup JbDone synchronization object is used, which counts the number of completed jobs. As I noted above, the code of the balancer is universal and in order for our balancer to work differently, it is enough for us to instantiate it with the appropriate handlers.

As well as in the last hand-made article) (http://habrahabr.ru/post/247727/) I implemented the application interface as a web interface.


For the test, I used the public dicom-archive 213.165.94.158:11112. You can download studies from it if there is a direct ip and if the port 11112 is open on the client side. I also checked the work on the free dcm4che archive archive sourceforge.net/projects/cdmedicpacsweb/files/latest/download?source=files .
I managed to build a working version for Linux, unfortunately I could not build it under Widows. The grassroot library was successfully built, but the error occurs when the application itself is linked.
cmd / ld: Malformed PE file: Unexpected flags for PE section.

Much has been written about this error here: github.com/golang/go/issues/4069 .
Unfortunately, I’m not so familiar with the intricacies of the build, so I only got the Linux version. Maybe the “habra-effect” will get this problem off the ground. For Windows users who want to check and see how it works, I prepared a CoreOS based virtual machine (https://yadi.sk/d/y81KC-tyfar6A). In the demo machine, our dicom client works as a systemd service.
If you have the desire, for example, you can implement a service that downloads research from various dicom-nodes, and puts it in a zip-archive for download. You can use json messages to control the service, just like our GUI does.
And you can do what I did: screw some html5 web viewer into our application.

Source: https://habr.com/ru/post/254581/


All Articles