@@ -658,3 +658,132 @@ func TestDockerStatsIntegrationReportsLiveCPU(t *testing.T) {
658658 t .Fatalf ("expected memory > 0 MB for a running container, got %v" , mem )
659659 }
660660}
661+
662+ // oomPtr returns a pointer to i for building catalog.ResourceLimits.OomScoreAdj
663+ // (a *int, so an absent value is distinguishable from an explicit 0) in tests.
664+ func oomPtr (i int ) * int { return & i }
665+
666+ // TestParseMemoryBytes pins the Docker-style binary-unit parsing used for the
667+ // docker.resources mem_limit/mem_reservation strings: k/m/g/t are 1024-based (so
668+ // "768m" is 768 MiB, not 768 MB), an optional trailing "b", surrounding
669+ // whitespace, mixed case and a fractional mantissa are accepted, and
670+ // empty/non-positive/garbage values are rejected so a bad service definition fails
671+ // fast instead of deploying an earner unbounded.
672+ func TestParseMemoryBytes (t * testing.T ) {
673+ cases := []struct {
674+ in string
675+ want int64
676+ wantErr bool
677+ }{
678+ {in : "768m" , want : 768 * 1024 * 1024 }, // mysterium
679+ {in : "2g" , want : 2 * 1024 * 1024 * 1024 }, // storj
680+ {in : "1536m" , want : 1536 * 1024 * 1024 }, // anyone-protocol
681+ {in : "256m" , want : 256 * 1024 * 1024 }, // honeygain/earnapp/proxyrack
682+ {in : "128m" , want : 128 * 1024 * 1024 }, // expendable earners
683+ {in : "512k" , want : 512 * 1024 }, // kibibytes
684+ {in : "1024" , want : 1024 }, // bare byte count, no suffix
685+ {in : "2gb" , want : 2 * 1024 * 1024 * 1024 }, // explicit trailing "b"
686+ {in : "768M" , want : 768 * 1024 * 1024 }, // case-insensitive suffix
687+ {in : " 256m " , want : 256 * 1024 * 1024 }, // surrounding whitespace
688+ {in : "1.5g" , want : 1536 * 1024 * 1024 }, // fractional mantissa
689+ {in : "" , wantErr : true }, // empty
690+ {in : "b" , wantErr : true }, // unit only, no number
691+ {in : "m" , wantErr : true }, // unit only, no number
692+ {in : "abc" , wantErr : true }, // not a number
693+ {in : "0m" , wantErr : true }, // non-positive
694+ {in : "-5m" , wantErr : true }, // negative
695+ }
696+ for _ , tc := range cases {
697+ got , err := parseMemoryBytes (tc .in )
698+ if tc .wantErr {
699+ if err == nil {
700+ t .Errorf ("parseMemoryBytes(%q) = %d, want error" , tc .in , got )
701+ }
702+ continue
703+ }
704+ if err != nil {
705+ t .Errorf ("parseMemoryBytes(%q) unexpected error: %v" , tc .in , err )
706+ continue
707+ }
708+ if got != tc .want {
709+ t .Errorf ("parseMemoryBytes(%q) = %d, want %d" , tc .in , got , tc .want )
710+ }
711+ }
712+ }
713+
714+ // TestApplyResourceLimitsSetsHostConfig verifies a protected service's
715+ // docker.resources block lands on the container HostConfig: the hard memory
716+ // ceiling is parsed to bytes, the negative OOM score is applied, and memory-swap
717+ // is deliberately left unset (0) so Docker derives it from Memory instead of
718+ // pinning swap.
719+ func TestApplyResourceLimitsSetsHostConfig (t * testing.T ) {
720+ hc := & container.HostConfig {}
721+ res := catalog.ResourceLimits {MemLimit : "2g" , OomScoreAdj : oomPtr (- 100 )}
722+ if err := applyResourceLimits (hc , res ); err != nil {
723+ t .Fatalf ("applyResourceLimits returned error: %v" , err )
724+ }
725+ if want := int64 (2 * 1024 * 1024 * 1024 ); hc .Memory != want {
726+ t .Fatalf ("Memory = %d, want %d" , hc .Memory , want )
727+ }
728+ if hc .OomScoreAdj != - 100 {
729+ t .Fatalf ("OomScoreAdj = %d, want -100" , hc .OomScoreAdj )
730+ }
731+ if hc .MemorySwap != 0 {
732+ t .Fatalf ("MemorySwap = %d, want 0 (must be left unset)" , hc .MemorySwap )
733+ }
734+ if hc .MemoryReservation != 0 {
735+ t .Fatalf ("MemoryReservation = %d, want 0 (not specified)" , hc .MemoryReservation )
736+ }
737+ }
738+
739+ // TestApplyResourceLimitsSetsReservation covers the optional soft limit: when
740+ // mem_reservation is present it is parsed and set alongside the hard mem_limit.
741+ func TestApplyResourceLimitsSetsReservation (t * testing.T ) {
742+ hc := & container.HostConfig {}
743+ res := catalog.ResourceLimits {MemLimit : "768m" , MemReservation : "256m" , OomScoreAdj : oomPtr (200 )}
744+ if err := applyResourceLimits (hc , res ); err != nil {
745+ t .Fatalf ("applyResourceLimits returned error: %v" , err )
746+ }
747+ if want := int64 (768 * 1024 * 1024 ); hc .Memory != want {
748+ t .Fatalf ("Memory = %d, want %d" , hc .Memory , want )
749+ }
750+ if want := int64 (256 * 1024 * 1024 ); hc .MemoryReservation != want {
751+ t .Fatalf ("MemoryReservation = %d, want %d" , hc .MemoryReservation , want )
752+ }
753+ if hc .OomScoreAdj != 200 {
754+ t .Fatalf ("OomScoreAdj = %d, want 200" , hc .OomScoreAdj )
755+ }
756+ }
757+
758+ // TestApplyResourceLimitsAbsentLeavesDefaults verifies an empty docker.resources
759+ // block touches nothing: memory stays unlimited (0) and OomScoreAdj stays at the
760+ // daemon default (0). A nil OomScoreAdj must be left alone rather than written as
761+ // an explicit 0.
762+ func TestApplyResourceLimitsAbsentLeavesDefaults (t * testing.T ) {
763+ hc := & container.HostConfig {}
764+ if err := applyResourceLimits (hc , catalog.ResourceLimits {}); err != nil {
765+ t .Fatalf ("applyResourceLimits returned error: %v" , err )
766+ }
767+ if hc .Memory != 0 || hc .MemoryReservation != 0 || hc .MemorySwap != 0 || hc .OomScoreAdj != 0 {
768+ t .Fatalf ("expected all limits unset, got Memory=%d MemoryReservation=%d MemorySwap=%d OomScoreAdj=%d" ,
769+ hc .Memory , hc .MemoryReservation , hc .MemorySwap , hc .OomScoreAdj )
770+ }
771+ }
772+
773+ // TestApplyResourceLimitsRejectsBadValue makes a malformed size fail the deploy
774+ // (rather than silently running the earner unbounded) and leaves HostConfig
775+ // unmodified.
776+ func TestApplyResourceLimitsRejectsBadValue (t * testing.T ) {
777+ for _ , res := range []catalog.ResourceLimits {
778+ {MemLimit : "notasize" },
779+ {MemReservation : "12x" },
780+ } {
781+ hc := & container.HostConfig {}
782+ if err := applyResourceLimits (hc , res ); err == nil {
783+ t .Errorf ("applyResourceLimits(%+v) = nil error, want error" , res )
784+ }
785+ if hc .Memory != 0 || hc .MemoryReservation != 0 {
786+ t .Errorf ("HostConfig mutated on error: %+v" , hc )
787+ }
788+ }
789+ }
0 commit comments