Jenkins Pipeline NotSerializableException: groovy.json.internal.LazyMap

80

Rozwiązany : Dzięki poniższej odpowiedzi od S.Richmond. Musiałem usunąć wszystkie zapisane mapy groovy.json.internal.LazyMaptypu, który oznaczał anulowanie zmiennych envServersi objectpo ich użyciu.

Dodatkowe : osoby szukające tego błędu mogą readJSONzamiast tego skorzystać z kroku potoku Jenkins - więcej informacji znajdziesz tutaj .


Próbuję użyć Jenkins Pipeline, aby pobrać dane wejściowe od użytkownika, które są przekazywane do zadania jako ciąg JSON. Następnie potok analizuje to za pomocą slurpera i wybieram ważne informacje. Następnie wykorzysta te informacje do wielokrotnego uruchomienia 1 zadania równolegle z różnymi parametrami zadania.

Dopóki nie dodam kodu poniżej, "## Error when below here is added"skrypt będzie działał poprawnie. Nawet kod poniżej tego punktu będzie działał samodzielnie. Ale po połączeniu otrzymuję poniższy błąd.

Powinienem zauważyć, że wyzwalane zadanie jest wywoływane i działa pomyślnie, ale pojawia się poniższy błąd i kończy się niepowodzeniem głównego zadania. Z tego powodu główne zadanie nie czeka na powrót wyzwolonego zadania. I mogłoby try / catch wokół build job:jednak chcę Głównym zadaniem czekać na wyzwalane zadania do końca.

Czy ktoś może tu pomóc? Jeśli potrzebujesz więcej informacji, daj mi znać.

Twoje zdrowie

def slurpJSON() {
return new groovy.json.JsonSlurper().parseText(BUILD_CHOICES);
}

node {
  stage 'Prepare';
  echo 'Loading choices as build properties';
  def object = slurpJSON();

  def serverChoices = [];
  def serverChoicesStr = '';

  for (env in object) {
     envName = env.name;
     envServers = env.servers;

     for (server in envServers) {
        if (server.Select) {
            serverChoicesStr += server.Server;
            serverChoicesStr += ',';
        }
     }
  }
  serverChoicesStr = serverChoicesStr[0..-2];

  println("Server choices: " + serverChoicesStr);

  ## Error when below here is added

  stage 'Jobs'
  build job: 'Dummy Start App', parameters: [[$class: 'StringParameterValue', name: 'SERVER_NAME', value: 'TestServer'], [$class: 'StringParameterValue', name: 'SERVER_DOMAIN', value: 'domain.uk'], [$class: 'StringParameterValue', name: 'APP', value: 'application1']]

}

Błąd:

java.io.NotSerializableException: groovy.json.internal.LazyMap
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:860)
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:569)
    at org.jboss.marshalling.river.BlockMarshaller.doWriteObject(BlockMarshaller.java:65)
    at org.jboss.marshalling.river.BlockMarshaller.writeObject(BlockMarshaller.java:56)
    at org.jboss.marshalling.MarshallerObjectOutputStream.writeObjectOverride(MarshallerObjectOutputStream.java:50)
    at org.jboss.marshalling.river.RiverObjectOutputStream.writeObjectOverride(RiverObjectOutputStream.java:179)
    at java.io.ObjectOutputStream.writeObject(Unknown Source)
    at java.util.LinkedHashMap.internalWriteEntries(Unknown Source)
    at java.util.HashMap.writeObject(Unknown Source)
...
...
Caused by: an exception which occurred:
    in field delegate
    in field closures
    in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup@5288c
Sunvic
źródło
Właśnie wpadłem na to sam. Czy zrobiłeś już dalsze postępy?
S.Richmond

Odpowiedzi:

71

Sam napotkałem to dzisiaj i dzięki pewnym brutalnym siłom odkryłem, jak to rozwiązać i potencjalnie dlaczego.

Prawdopodobnie najlepiej zacząć od tego, dlaczego:

Jenkins ma paradygmat, w którym wszystkie zadania można przerwać, wstrzymać i wznowić poprzez ponowne uruchomienie serwera. Aby to osiągnąć, potok i jego dane muszą być w pełni serializowalne - tj. Musi mieć możliwość zapisania stanu wszystkiego. Podobnie musi być w stanie serializować stan zmiennych globalnych między węzłami i zadaniami podrzędnymi w kompilacji, co moim zdaniem dzieje się dla ciebie i dla mnie i dlaczego występuje tylko wtedy, gdy dodasz ten dodatkowy krok kompilacji.

Z jakiegoś powodu JSONObject nie jest domyślnie serializowalny. Nie jestem programistą Java, więc nie mogę niestety powiedzieć nic więcej na ten temat. Istnieje wiele odpowiedzi na temat tego, jak można to naprawić, chociaż nie wiem, jak mają one zastosowanie do Groovy'ego i Jenkinsa. Zobacz ten post, aby uzyskać więcej informacji.

Jak to naprawić:

Jeśli wiesz, jak to zrobić, możesz w jakiś sposób uczynić JSONObject możliwym do serializacji. W przeciwnym razie możesz rozwiązać ten problem, upewniając się, że żadne zmienne globalne nie są tego typu.

Spróbuj cofnąć ustawienie objectzmiennej lub zawinąć ją w metodę, aby jej zakres nie był globalny.

S.Richmond
źródło
2
Dzięki, to wskazówka, której potrzebowałem, aby to rozwiązać. Chociaż wypróbowałem już twoją sugestię, to sprawiło, że spojrzałem ponownie i nie brałem pod uwagę, że przechowuję części mapy w innych zmiennych - one powodowały błędy. Więc musiałem je również rozbroić. Zmienię moje pytanie, aby uwzględnić prawidłowe zmiany w kodzie. Pozdrawiam
Sunvic
1
Jest to oglądane ~ 8 razy dziennie. Czy moglibyście podać bardziej szczegółowy przykład wdrożenia tego rozwiązania?
Jordan Stefanelli
1
Nie ma prostego rozwiązania, ponieważ zależy to od tego, co zrobiłeś. Podane tutaj informacje, a także rozwiązanie, które @Sunvic dodał na początku swojego postu, wystarczyły, aby doprowadzić do rozwiązania dla własnego kodu.
S.Richmond
1
Poniższe rozwiązanie, używając JsonSlurperClassic, naprawiło dokładnie ten sam problem, który miałem, prawdopodobnie powinno być tutaj zatwierdzonym wyborem. Ta odpowiedź ma zalety, ale nie jest właściwym rozwiązaniem tego konkretnego problemu.
Kwarc
@JordanStefanelli Opublikowałem kod mojego rozwiązania. Zobacz moją odpowiedź poniżej
Nils El-Himoud
127

Użyj JsonSlurperClassiczamiast tego.

Od Groovy 2.3 ( uwaga: Jenkins 2.7.1 używa Groovy 2.4.7 ) JsonSlurperzwraca LazyMapzamiast HashMap. To sprawia, że ​​nowa implementacja JsonSlurper nie jest bezpieczna dla wątków i nie jest możliwa do serializacji. To sprawia, że ​​jest bezużyteczny poza funkcjami @NonDSL w skryptach DSL potoku.

Możesz jednak wrócić do tego, groovy.json.JsonSlurperClassicktóry obsługuje stare zachowanie i może być bezpiecznie używany w skryptach potoku.

Przykład

import groovy.json.JsonSlurperClassic 


@NonCPS
def jsonParse(def json) {
    new groovy.json.JsonSlurperClassic().parseText(json)
}

node('master') {
    def config =  jsonParse(readFile("config.json"))

    def db = config["database"]["address"]
    ...
}    

ps. Nadal będziesz musiał zatwierdzić, JsonSlurperClassiczanim będzie można go wywołać.

luka5z
źródło
2
Czy mógłbyś mi powiedzieć, jak zatwierdzić JsonSlurperClassic?
mybecks
7
Administrator Jenkins będzie musiał przejść do Manage Jenkins »In-process Script Approval.
luka5z
Niestety dostaję tylkohudson.remoting.ProxyException: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: Script1.groovy: 24: unable to resolve class groovy.json.JsonSlurperClassic
dvtoever
13
JsonSluperClassic .. Ta nazwa wiele mówi o obecnym stanie rozwoju oprogramowania
Marcos Brigante
1
Dziękuję bardzo za to szczegółowe wyjaśnienie. Zaoszczędziłeś mi dużo czasu. To rozwiązanie działa jak urok w moim potoku Jenkinsa.
Sathish Prakasam
16

EDYCJA: Jak wskazał @Sunvic w komentarzach, poniższe rozwiązanie nie działa tak, jak jest w przypadku tablic JSON.

Poradziłem sobie z tym, używając, JsonSlurpera następnie tworząc nowe HashMapz leniwych wyników. HashMapjest Serializable.

Uważam, że wymagało to białej listy zarówno new HashMap(Map)platformy JsonSlurper.

@NonCPS
def parseJsonText(String jsonText) {
  final slurper = new JsonSlurper()
  return new HashMap<>(slurper.parseText(jsonText))
}

Ogólnie polecam użycie wtyczki Pipeline Utility Steps , ponieważ ma ona readJSONkrok, który może obsługiwać pliki w obszarze roboczym lub tekst.

mkobit
źródło
1
Nie działało mnie - ciągle pojawiał się błąd Could not find matching constructor for: java.util.HashMap(java.util.ArrayList). Dokumentacja sugeruje, że powinien wypluć listę lub mapę - jak skonfigurować zwracanie mapy?
Sunvic
@Sunvic Dobry chwyt, dane, które analizowaliśmy, to zawsze obiekty, nigdy tablice JSON. Czy próbujesz przeanalizować tablicę JSON?
mkobit
Ach tak, to tablica JSON, to wszystko.
Sunvic
Zarówno ta odpowiedź, jak i poniżej, na Jenkins, wywołały
odrzucenie odrzucenia,
@yiwen Wspomniałem, że wymaga to białej listy administratora, ale może odpowiedź mogłaby zostać wyjaśniona, co to oznacza?
mkobit,
8

Chcę zagłosować za jedną z odpowiedzi: polecam po prostu użycie wtyczki Pipeline Utility Steps, ponieważ ma krok readJSON, który może obsługiwać pliki w obszarze roboczym lub tekst: https://jenkins.io/doc/pipeline/steps / pipeline-utility-steps / # readjson-read-json-from-files-in-the-workspace

script{
  def foo_json = sh(returnStdout:true, script: "aws --output json XXX").trim()
  def foo = readJSON text: foo_json
}

NIE wymaga to żadnej białej listy ani dodatkowych rzeczy.

Regnoult
źródło
6

Oto szczegółowa odpowiedź, o którą poproszono.

Niespokojony działał dla mnie:

String res = sh(script: "curl --header 'X-Vault-Token: ${token}' --request POST --data '${payload}' ${url}", returnStdout: true)
def response = new JsonSlurper().parseText(res)
String value1 = response.data.value1
String value2 = response.data.value2

// unset response because it's not serializable and Jenkins throws NotSerializableException.
response = null

Odczytuję wartości z przeanalizowanej odpowiedzi i kiedy nie potrzebuję już obiektu, wyłączam go.

Nils El-Himoud
źródło
5

Nieco bardziej uogólniona forma odpowiedzi z @mkobit, która pozwoliłaby na dekodowanie tablic, a także map, to:

import groovy.json.JsonSlurper

@NonCPS
def parseJsonText(String json) {
  def object = new JsonSlurper().parseText(json)
  if(object instanceof groovy.json.internal.LazyMap) {
      return new HashMap<>(object)
  }
  return object
}

UWAGA: Należy pamiętać, że spowoduje to tylko konwersję obiektu LazyMap najwyższego poziomu na HashMap. Wszelkie zagnieżdżone obiekty LazyMap nadal tam będą i nadal będą powodować problemy z Jenkins.

TomDotTom
źródło
2

Sposób implementacji wtyczki potoku ma dość poważne konsekwencje dla nietrywialnego kodu Groovy'ego. Ten link wyjaśnia, jak uniknąć możliwych problemów: https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md#serializing-local-variables

W twoim konkretnym przypadku rozważyłbym dodanie @NonCPSadnotacji slurpJSONi zwrócenie mapy map zamiast obiektu JSON. Nie tylko kod wygląda na bardziej przejrzysty, ale jest też bardziej wydajny, zwłaszcza jeśli ten kod JSON jest złożony.

Marcin Płonka
źródło
2

Zgodnie z najlepszymi praktykami opublikowanymi na blogu Jenkins ( najlepsze praktyki dotyczące skalowalności potoków ), zdecydowanie zaleca się używanie narzędzi wiersza poleceń lub skryptów do tego rodzaju pracy:

Gotcha: szczególnie unikaj parsowania Pipeline XML lub JSON przy użyciu XmlSlurper i JsonSlurper firmy Groovy! Zdecydowanie wolę narzędzia wiersza poleceń lub skrypty.

ja. Implementacje Groovy są złożone, a co za tym idzie, bardziej kruche w użyciu Pipeline.

ii. XmlSlurper i JsonSlurper mogą przenosić wysoki koszt pamięci i procesora w potokach

iii. xmllint i xmlstartlet to narzędzia wiersza poleceń oferujące wyodrębnianie XML za pośrednictwem xpath

iv. jq oferuje tę samą funkcjonalność dla formatu JSON

v. Te narzędzia do ekstrakcji mogą być połączone z curl lub wget w celu pobierania informacji z interfejsu API HTTP

W ten sposób wyjaśnia, dlaczego większość rozwiązań proponowanych na tej stronie jest domyślnie blokowana przez piaskownicę wtyczki skryptów bezpieczeństwa Jenkins.

Filozofia języka Groovy jest bliższa Bashowi niż Pythonowi czy Javie. Oznacza to również, że wykonywanie skomplikowanej i ciężkiej pracy w rodzimym Groovym nie jest naturalne.

Biorąc to pod uwagę, osobiście zdecydowałem się skorzystać z:

sh('jq <filters_and_options> file.json')

Aby uzyskać dodatkową pomoc, zobacz Podręcznik jq i Wybierz obiekty z postem jq stackoverflow .

Jest to nieco sprzeczne z intuicją, ponieważ Groovy zapewnia wiele ogólnych metod, których nie ma na domyślnej białej liście.

Jeśli mimo wszystko zdecydujesz się używać języka Groovy do większości swojej pracy, z włączoną piaskownicą i czystą (co nie jest łatwe, ponieważ nie jest naturalne), radzę sprawdzić białe listy wersji wtyczki skryptu bezpieczeństwa, aby dowiedzieć się, jakie masz możliwości: Skrypt białe listy wtyczek bezpieczeństwa

vhamon
źródło
2

Możesz użyć następującej funkcji, aby przekonwertować LazyMap na zwykły LinkedHashMap (zachowa kolejność oryginalnych danych):

LinkedHashMap nonLazyMap (Map lazyMap) {
    LinkedHashMap res = new LinkedHashMap()
    lazyMap.each { key, value ->
        if (value instanceof Map) {
            res.put (key, nonLazyMap(value))
        } else if (value instanceof List) {
            res.put (key, value.stream().map { it instanceof Map ? nonLazyMap(it) : it }.collect(Collectors.toList()))
        } else {
            res.put (key, value)
        }
    }
    return res
}

... 

LazyMap lazyMap = new JsonSlurper().parseText (jsonText)
Map serializableMap = nonLazyMap(lazyMap);

lub lepiej użyj kroku readJSON, jak zauważono we wcześniejszych komentarzach:

Map serializableMap = readJSON text: jsonText
Siergiej P.
źródło
1

Inne pomysły w tym poście były pomocne, ale nie wszystko, czego szukałem - więc wyodrębniłem części, które pasowały do ​​moich potrzeb i dodałem trochę mojego własnego magixa ...

def jsonSlurpLaxWithoutSerializationTroubles(String jsonText)
{
    return new JsonSlurperClassic().parseText(
        new JsonBuilder(
            new JsonSlurper()
                .setType(JsonParserType.LAX)
                .parseText(jsonText)
        )
        .toString()
    )
}

Tak, jak zauważyłem w moim własnym git commit w kodzie, „Dziwnie nieefektywny, ale mały współczynnik: rozwiązanie slurp JSON” (z którym jestem w porządku w tym celu). Aspekty, które musiałem rozwiązać:

  1. Całkowicie uniknij java.io.NotSerializableExceptionproblemu, nawet jeśli tekst JSON definiuje zagnieżdżone kontenery
  2. Działa zarówno dla kontenerów map, jak i tablic
  3. Obsługa parsowania LAX (najważniejsza część w mojej sytuacji)
  4. Łatwy do wdrożenia (nawet z niewygodnymi zagnieżdżonymi konstruktorami, których się nie stosuje @NonCPS)
Stevel
źródło
1

Noob błąd z mojej strony. Przeniesiono czyjś kod ze starej wtyczki potoku, jenkins 1.6? na serwer z najnowszą wersją 2.x jenkins.

Niepowodzenie z tego powodu: „java.io.NotSerializableException: groovy.lang.IntRange” Wielokrotnie czytałem i czytałem ten post z powodu powyższego błędu. Zrealizowane: for (num in 1..numSlaves) {IntRange - nie serializowalny typ obiektu.

Przepisano w prostej formie: for (num = 1; num <= numSlaves; num ++)

Wszystko jest dobrze ze światem.

Nie używam bardzo często Java ani Groovy.

Dzięki chłopaki.

mpechner
źródło
0

Znalazłem łatwiejszy sposób w dokumentach poza rurociągiem Jenkinsa

Przykład pracy

import groovy.json.JsonSlurperClassic 


@NonCPS
def jsonParse(def json) {
    new groovy.json.JsonSlurperClassic().parseText(json)
}

@NonCPS
def jobs(list) {
    list
        .grep { it.value == true  }
        .collect { [ name : it.key.toString(),
                      branch : it.value.toString() ] }

}

node {
    def params = jsonParse(env.choice_app)
    def forBuild = jobs(params)
}

Ze względu na ograniczenia przepływu pracy - tj. JENKINS-26481 - nie jest możliwe użycie domknięć Groovy lub składni zależnej od domknięć, więc nie można> wykonać standardu Groovy polegającego na użyciu .collectEntries na liście i generowaniu kroków jako wartości dla wynikowych wpisów. Nie możesz również użyć standardowej składni> Java dla pętli For - tj. „For (String s: strings)” - i zamiast tego musisz użyć starej szkoły pętli opartych na licznikach.

Kirill K.
źródło
1
Poleciłbym zamiast tego użyć kroku potoku Jenkins readJSON - więcej informacji znajdziesz tutaj .
Sunvic